Compare commits
1451 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 97e6b9e42b | |||
| 1e2da25d58 | |||
| 66ba3430fd | |||
| 1abbce4daa | |||
| d8430a0885 | |||
| c6054648d3 | |||
| 74d3183da3 | |||
| cc9a35355d | |||
| 90e4e334c4 | |||
| b51ec78ff5 | |||
| 8ef9b152f0 | |||
| 08b8b31e47 | |||
| 1bec167be6 | |||
| 641778b169 | |||
| 73a651f3aa | |||
| e84911c25d | |||
| 467e036411 | |||
| c643eedbc8 | |||
| 542ab0a286 | |||
| d31173fb9d | |||
| a8b49f713b | |||
| c65a6fb7da | |||
| 17808a3c1a | |||
| cc046010ff | |||
| 0c5b993f20 | |||
| 1c63f795f4 | |||
| ba14fc80a9 | |||
| 6015c27e1b | |||
| f4ae9f4a8c | |||
| dad1cb0c3c | |||
| 608c7f4613 | |||
| 991153997b | |||
| e8bc3e695b | |||
| 98fabf382c | |||
| 833eec4833 | |||
| 7aa258bc97 | |||
| a4b6c50c0e | |||
| 47deca8859 | |||
| ca01ee743b | |||
| 27ca1d8bfe | |||
| 05a6abc1ba | |||
| 29ba8509bc | |||
| a290a3d93e | |||
| 4fe83d732a | |||
| 5f7a564b33 | |||
| 0de22d4f0d | |||
| 15149d1d07 | |||
| 5b54fc3b27 | |||
| beed3883a8 | |||
| 254c2690e5 | |||
| 2839304dc5 | |||
| 48b4b989b1 | |||
| 96306afecf | |||
| d267988b55 | |||
| 5780a6fc25 | |||
| 28eddf4aea | |||
| d8a7a700c4 | |||
| fc6b3331ac | |||
| 2210e6389f | |||
| ebf692e6a3 | |||
| 5422c1c45f | |||
| f5c63aa958 | |||
| 4b0bf16916 | |||
| b8a5172d70 | |||
| c4735e5603 | |||
| e601f6b57a | |||
| 272e7e5b98 | |||
| 5304c77548 | |||
| 2e57869c5f | |||
| cde51445a1 | |||
| 9b144b58bb | |||
| 1796bf8fb8 | |||
| c587cf7953 | |||
| 922e3911d8 | |||
| aadbaf0f75 | |||
| 4b95053651 | |||
| f66aaa09b0 | |||
| f7b352f595 | |||
| 3cfb888a5f | |||
| d9d0ea3168 | |||
| 408d62c09d | |||
| a4c61f3dd9 | |||
| f3321ff267 | |||
| 2ef242099b | |||
| 3497c277e3 | |||
| 74c47b8dd9 | |||
| c860ffd315 | |||
| 1b13f4abb2 | |||
| a8c882e343 | |||
| 99c51bb205 | |||
| 142e0da7c0 | |||
| 226297c810 | |||
| 13167fe050 | |||
| 9665b3a793 | |||
| 95111476f5 | |||
| fcfdd248b6 | |||
| df792a57c8 | |||
| 41e68d070d | |||
| 42231c79d9 | |||
| 41cc66a9f4 | |||
| 71dcf9e8c1 | |||
| 38232c3182 | |||
| 13288c6620 | |||
| 11b855109a | |||
| 4110b91b45 | |||
| c90c50a4b6 | |||
| ef5322ebd5 | |||
| e82ae7cb88 | |||
| 0726783465 | |||
| 8a7007c7b7 | |||
| 606732c987 | |||
| b664674a06 | |||
| 9b201d8475 | |||
| 66d553f6a7 | |||
| 94a1a0f108 | |||
| e3af901caf | |||
| ad5f5c3e39 | |||
| ddc6aacaf3 | |||
| a88164319b | |||
| 93aeab2c01 | |||
| 0be4bc6cb4 | |||
| d12984cb3a | |||
| 2fc8986bd8 | |||
| f8b8588413 | |||
| 3519ca6018 | |||
| cffc39dbfd | |||
| c75dc76526 | |||
| e6d5e35ff0 | |||
| cb05b8be96 | |||
| 8735237626 | |||
| 67a9dcc37f | |||
| 18825670e4 | |||
| 17dc6045b0 | |||
| c8ec7893e1 | |||
| 837fda814b | |||
| e306b91e37 | |||
| 50377cd9ee | |||
| 969336f203 | |||
| ae14eae711 | |||
| 5603831f5e | |||
| 760cfe2463 | |||
| 509243f874 | |||
| 311544b5aa | |||
| 1cf9ff157a | |||
| 8e54749e2e | |||
| 9adc78f287 | |||
| 6309a77b63 | |||
| 1eb8b5c137 | |||
| e78c5c8cef | |||
| 01662cfa8d | |||
| 1bda3ccd53 | |||
| d9896f0b48 | |||
| ac13da1dc6 | |||
| 20988fb35e | |||
| 5e6c0853c1 | |||
| 7a467d6346 | |||
| 505953753c | |||
| e4ddd12b7b | |||
| e1decbf7c2 | |||
| cdd1712b85 | |||
| 356259f0ec | |||
| 7928fe6598 | |||
| bcc8c8492f | |||
| d26f6618ec | |||
| af919e77e7 | |||
| c0aa07f736 | |||
| ae3e5c5092 | |||
| a2665d12f6 | |||
| 00edf57ebc | |||
| f1bd6799a7 | |||
| 4b12b44f8b | |||
| f9fc840c3c | |||
| bfe3c62db7 | |||
| aa7cc12876 | |||
| 9a2476b095 | |||
| 297fa9a4c6 | |||
| 6fbe0b2d40 | |||
| cae401ce55 | |||
| 8874888846 | |||
| 1f5a27a425 | |||
| 81a686b2ec | |||
| 19dd5b8d00 | |||
| 351b6da4d1 | |||
| 078b36eea6 | |||
| f003f0d48c | |||
| 616f0a7420 | |||
| 208d816c64 | |||
| 43cb9e384f | |||
| f28bd1851a | |||
| 4807a6d503 | |||
| 2e8e4b330f | |||
| ff433f2fec | |||
| 0f92254402 | |||
| 230b69356c | |||
| 30f1600063 | |||
| f5beddd68e | |||
| c66a3b5b11 | |||
| 0fd4d31d6b | |||
| 86f9008804 | |||
| 380fe19a20 | |||
| 9e6eb3c71e | |||
| 5ea2fcf592 | |||
| 7a5bf96916 | |||
| cb45759d5e | |||
| 2ce6cb9d0b | |||
| b6ddfba700 | |||
| 22d9e278c5 | |||
| ee06f557dc | |||
| 11b6455c00 | |||
| 6cbbfd9b46 | |||
| 1d31bfb5ae | |||
| 1d6db35c5c | |||
| 6412482c06 | |||
| cc039655ae | |||
| fef4612122 | |||
| 4e7c458b7f | |||
| dbb51e50bd | |||
| ab5caf9701 | |||
| 0aeeb2a933 | |||
| 4c8b7e6ad0 | |||
| c3c9e2c175 | |||
| d821be41df | |||
| 103a7a2fa4 | |||
| f17122d1ca | |||
| 131027d1d9 | |||
| be0851cdd1 | |||
| 8a430dccc4 | |||
| 79208f811a | |||
| 14884d0111 | |||
| ed727a7f26 | |||
| 10d25ff091 | |||
| 587f1d4a93 | |||
| 63612f4474 | |||
| 5e67b1a08f | |||
| 8e4557af18 | |||
| cc4cbe0b28 | |||
| f6f009083f | |||
| 2744eb97b6 | |||
| 3691f16d8c | |||
| c0d23770b5 | |||
| 2fb69a5992 | |||
| 52e664dd18 | |||
| d923b6ea02 | |||
| f153cb3fff | |||
| 2cff73862d | |||
| 316ea5428e | |||
| 45e397b6c4 | |||
| 74148e3782 | |||
| 91f50946b5 | |||
| 83fc54fcda | |||
| d1cfc69b5c | |||
| 012e2d5aea | |||
| 0a5f7d3be2 | |||
| eff37aac03 | |||
| 4e23bfb507 | |||
| d1b167c002 | |||
| 0b51713d95 | |||
| 0306585cf8 | |||
| fce32881e3 | |||
| bb79ed6e92 | |||
| 4f44423239 | |||
| d5f01c8ab0 | |||
| 276f9651a1 | |||
| 39b1fad44e | |||
| 237dd45cde | |||
| e4b6494046 | |||
| 2300cf6bf0 | |||
| 6fb2d6a5f5 | |||
| 0a5cd155e2 | |||
| 9b3d30959d | |||
| b8339a4e4c | |||
| ebc3dfb75b | |||
| 1293967512 | |||
| f0abfa16ed | |||
| f1885509d8 | |||
| cbfad69d22 | |||
| 533d788b6a | |||
| 48d1931368 | |||
| 808e8a5708 | |||
| 396edfef41 | |||
| 8cad524217 | |||
| e377f678c8 | |||
| bdaca5aeda | |||
| 7ca8554d7e | |||
| fda91fbfe3 | |||
| 259d4c3474 | |||
| 2a4a54d60b | |||
| 6d55ae8180 | |||
| deda2a194e | |||
| 7b1078dad2 | |||
| 70a46c0782 | |||
| 3f48a97668 | |||
| 5391ca7137 | |||
| 80d463aba8 | |||
| 3e3ab4fdab | |||
| 97ca5c5443 | |||
| 9cd3bbbd48 | |||
| d092ef2972 | |||
| b6182c3466 | |||
| 902cb66cc4 | |||
| bf690a941c | |||
| bc5a679bb6 | |||
| 2a3109577c | |||
| 5631ea2ac3 | |||
| b84dd14ced | |||
| 2899f6836b | |||
| 24c89ac381 | |||
| 87a3aed16d | |||
| cd14b2a768 | |||
| df79998990 | |||
| 25d0965696 | |||
| e886f981a0 | |||
| 81f66b65de | |||
| 61c934c360 | |||
| ed1a667e09 | |||
| 3f27db8c77 | |||
| b975c0332e | |||
| a0ae839dd3 | |||
| 43c7918854 | |||
| e16a95837c | |||
| 3d6254f2cc | |||
| 367dccfe95 | |||
| 7c564f9497 | |||
| 546ba16bb2 | |||
| a958fc6d9e | |||
| 3f7433f4da | |||
| 37ff3f5dbf | |||
| d20479e88d | |||
| f9bf5b81c9 | |||
| 43e1e9f0ed | |||
| e4357abcfd | |||
| 3e26965ad0 | |||
| 6035136358 | |||
| 1472bf4dbe | |||
| 2bae41eed1 | |||
| ad4bb875ae | |||
| 0a145ecdb5 | |||
| 95a7907d72 | |||
| f019d31f99 | |||
| 3d7fa6cdae | |||
| 7e5302c913 | |||
| 4cf64a0418 | |||
| c8289f5278 | |||
| ea93bc8d1a | |||
| b1df999efb | |||
| 320c7ab8ff | |||
| b706c439c8 | |||
| f4e11fcbcc | |||
| 3dea971055 | |||
| 252be18c2f | |||
| ba84861628 | |||
| d974fb24a0 | |||
| b05b145911 | |||
| 06bffb905e | |||
| 8ea8a3474f | |||
| 7609513a07 | |||
| 8b1ffb412c | |||
| a137466ed1 | |||
| 852d016203 | |||
| 49765498e7 | |||
| 02c2ddd7d8 | |||
| e643b65d98 | |||
| e3b011003d | |||
| b6b1a9362c | |||
| 522f2f803b | |||
| 4d7109fa6b | |||
| c31043a760 | |||
| fed0634a51 | |||
| d4f643b3f4 | |||
| 18b17cdd67 | |||
| 74dab07d3f | |||
| 91e52d2fc0 | |||
| 4ba6f4a613 | |||
| df68cc5927 | |||
| 4201d9aa4e | |||
| 7c8bde24b3 | |||
| 158344f24f | |||
| 78159fda3f | |||
| 8988e02793 | |||
| 6e7642e126 | |||
| 47bea6197a | |||
| ea17b3e9ea | |||
| da719a0a6b | |||
| f654527311 | |||
| 880dea1bef | |||
| 021915d9d4 | |||
| 1fb0102328 | |||
| 1fc2a2adda | |||
| ddc1024611 | |||
| 1de041351c | |||
| f64cf0ad02 | |||
| 46ef56efbd | |||
| 7b04d1e006 | |||
| 511d4544b6 | |||
| 140893a9b2 | |||
| 427773f4cb | |||
| dfeb5d0272 | |||
| 304ba63771 | |||
| 1698f1737d | |||
| 9d8a797c54 | |||
| 4d0c48619d | |||
| e95d212628 | |||
| 936e6ffa41 | |||
| f2a3260b8f | |||
| 5c8b2b37d4 | |||
| 44e4b615ba | |||
| 60a852815b | |||
| 7c7d7e609a | |||
| 0da5be9448 | |||
| 1ebb69c89c | |||
| 99af1c4607 | |||
| e1caacce34 | |||
| e2b6a92743 | |||
| 982c8cf872 | |||
| 1e928ac1a8 | |||
| a17ab1511d | |||
| 07c55df766 | |||
| 06ee7b4de9 | |||
| 0c520d5f50 | |||
| 522867a2dc | |||
| d56d985734 | |||
| 1d404b4f51 | |||
| c1d3cc88bc | |||
| 1242e2ed58 | |||
| f61dccb90a | |||
| 106da7e7f1 | |||
| 4774ad658b | |||
| 6035fe5318 | |||
| 452ac69fb6 | |||
| 3ec604f049 | |||
| f5c093960a | |||
| 984fa394a5 | |||
| db12a5b9e7 | |||
| dc57ee16c9 | |||
| 975542dab2 | |||
| 5ad2be88c1 | |||
| 76ec5d4476 | |||
| 62237d5f5f | |||
| eeb2ae56d7 | |||
| bb1751f442 | |||
| a4241f9549 | |||
| 693d479b0f | |||
| cf5c59fb78 | |||
| 311055a4c9 | |||
| 02d5e209d9 | |||
| e4ebaf2935 | |||
| 97fe8395e6 | |||
| d7b319b6c9 | |||
| 1fdf6a8a72 | |||
| 3425894c24 | |||
| 3e6caab023 | |||
| 7e23919c0d | |||
| 6b7a5af7ae | |||
| 74bfec92f6 | |||
| 30030a0ca5 | |||
| 78590f96f1 | |||
| cc485708c5 | |||
| daeca0fb0b | |||
| 5b5704bd75 | |||
| 762be8bfba | |||
| b224f07e00 | |||
| b1a902f92e | |||
| aeadb6fc8b | |||
| e8aeecb27c | |||
| 29131227b5 | |||
| 9b3a61e5cc | |||
| 4bc9113b96 | |||
| 45e0ac6066 | |||
| 4047a3e8f6 | |||
| d4d8312016 | |||
| e60df664c1 | |||
| 916c586cbc | |||
| a903b2cdfa | |||
| 79f3a8e497 | |||
| 5905eb1923 | |||
| 6dd3d898aa | |||
| 7c86ecf3b5 | |||
| 45e1376a8f | |||
| 07f3544ba5 | |||
| 4f93539673 | |||
| 095dfdb54f | |||
| eaac2b9332 | |||
| e63547140b | |||
| 99aa7b4093 | |||
| b4e6dcdb8c | |||
| 2d182c3cf4 | |||
| 05feb8c9f3 | |||
| 3adfdb5aac | |||
| c0ec2e03ff | |||
| e128704552 | |||
| af2aba9a50 | |||
| dbdd33d050 | |||
| d230636a0e | |||
| e7d639af33 | |||
| b97ca4f699 | |||
| 1657330c57 | |||
| dd8bbf2cbe | |||
| 53065c9dd3 | |||
| 27bcd6b9b6 | |||
| bc190cbdfb | |||
| 755b384b76 | |||
| a3f03f5577 | |||
| 57f29262e3 | |||
| ac5549ccc4 | |||
| d3ba0af4f5 | |||
| 6312515b7b | |||
| 41cdbe93b1 | |||
| 702eb4d8f3 | |||
| 7581ea016e | |||
| 4938b2a72a | |||
| 815258e8be | |||
| adb211c58a | |||
| cdeae1e1d4 | |||
| dfbe255191 | |||
| f8f1c1f8a3 | |||
| 4e5809a1dc | |||
| aef5fa47fc | |||
| ca751c27d1 | |||
| 95690fe046 | |||
| 1fd17706e4 | |||
| 082400abe8 | |||
| e90741b6bc | |||
| 71352ee39a | |||
| caa6b38673 | |||
| 49c23c9562 | |||
| 94424ea288 | |||
| 956119be2a | |||
| e26decd77d | |||
| 4d7b41bf2a | |||
| e6cfa30ed4 | |||
| ea46d16836 | |||
| 128dbb2ca7 | |||
| 097985f087 | |||
| 26fa50ead2 | |||
| 14a9c5a9b2 | |||
| 55432e5c33 | |||
| a2f6a30ad1 | |||
| 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 | |||
| e15c95561c | |||
| f4ed39401c | |||
| c3fb998254 | |||
| d9e9bcc731 | |||
| 110ff6daec | |||
| 7d83344e73 | |||
| 06d93398fb | |||
| b05328888e | |||
| 19598f4a10 | |||
| cd7102a5df | |||
| 9bf1f6b624 | |||
| f1681ad405 | |||
| e0a70a23b0 | |||
| 677dfb491a | |||
| 07a9b38c8d | |||
| 53b78c4a94 | |||
| 2057f08e40 | |||
| 7bc843aa9a | |||
| 0a8855e020 | |||
| 445d07c5e4 | |||
| 125e7d14de | |||
| 4917f5cb0c | |||
| 2504b37eb7 | |||
| b0d3bf4bd2 | |||
| e640384fd8 | |||
| adf7af6b84 | |||
| beb4155aab | |||
| 3337766ae6 | |||
| dc135dc78b | |||
| b39c51beea | |||
| 971339e49c | |||
| 9db0431e96 | |||
| d667f2ab34 | |||
| 9b895a8fd7 | |||
| 66d7e18a71 | |||
| 4643155908 | |||
| c6c3303a99 | |||
| 1aea558c40 | |||
| 316c7e1f63 | |||
| 8364befc91 | |||
| 47c25d0e71 | |||
| 422d574c6c | |||
| 3400c07e77 | |||
| 79bb10b14e | |||
| e7563fcfb5 | |||
| 43f0046827 | |||
| ef424dd9e3 | |||
| 37902b6d1f | |||
| 92e692f69a | |||
| b6e0e12cab | |||
| 6eb7e421e0 | |||
| fbb9352546 | |||
| 6f5c4f3a09 | |||
| 1e91cec8d2 | |||
| 17262e12af | |||
| 56a954c235 | |||
| da5eced769 | |||
| 71f2eccab4 | |||
| a5162c1f5b | |||
| 53c82fd477 | |||
| 2867f8d09c | |||
| 5b902171c8 | |||
| c40c5b6b94 | |||
| b66c313b05 | |||
| 48287cdc4b | |||
| 750c768848 | |||
| 4b512d64d9 | |||
| e121210e96 | |||
| 5a87608fa6 | |||
| e819abe789 | |||
| eb718d14d1 | |||
| 564896e940 | |||
| d91c3875b0 | |||
| 1fe2edb9f4 | |||
| 91977e1226 | |||
| 948018c7de | |||
| f2350680c8 | |||
| 4cc424e079 | |||
| 49d26fcf61 | |||
| 9fde3b21d4 | |||
| 6952649058 | |||
| b12a717cea | |||
| 8e1e222656 | |||
| 8f7775bb69 | |||
| 0477161af2 | |||
| 3b387db5b5 | |||
| d85d0e4e36 | |||
| b74b086f98 | |||
| e1d20b822f | |||
| e98997f230 | |||
| 82e17030d4 | |||
| d1905ee2b1 | |||
| 310b2bcee1 | |||
| d55eebce19 | |||
| 03507968ee | |||
| 56769e4518 | |||
| fba589d6fe | |||
| ff5b22b5dc | |||
| 14de2e0b23 | |||
| 880e331c99 | |||
| cf794588df | |||
| d362598d83 | |||
| 536e98642d | |||
| baae0d088c | |||
| 202feb12d7 | |||
| d2ab7c059f | |||
| e1ef117dcd | |||
| fb3186d575 | |||
| b913873951 | |||
| bbdcb42316 | |||
| 5438637224 | |||
| 970a058ba3 | |||
| 5d295db1b7 | |||
| b22aac7ca2 | |||
| eef29a4611 | |||
| 131952ed37 | |||
| 2e95c2550e | |||
| 0db6e0d693 | |||
| 27198ad4e0 | |||
| 3cbb6ebee6 | |||
| 2308df65b5 | |||
| c8f82cc991 | |||
| a6e1b905ed | |||
| 9432e9a806 | |||
| d90280a10f | |||
| db30b7f807 | |||
| 20910b1562 | |||
| 5659a049e5 | |||
| 085812d859 | |||
| e60a4669ac | |||
| ac84907a98 | |||
| 04ee69e8e2 | |||
| a2445aca6a | |||
| 336a58b23d | |||
| 5a5cf9bd4d | |||
| 44d262f457 | |||
| 27fda43373 | |||
| b3de3b8405 | |||
| 633918aa40 | |||
| 3bc59e87d3 | |||
| af7d9e9dca | |||
| 3b8884dc84 | |||
| 92158684c0 | |||
| b37de9cf24 | |||
| cd2dff5559 | |||
| d6a85af15c | |||
| 0d333879ca | |||
| 905db3c73a | |||
| c5dfad8be8 | |||
| c9454a3832 | |||
| c732d475f1 | |||
| a4b7e0fd46 | |||
| bcee069b32 | |||
| 182208f8c3 | |||
| 24befed17b | |||
| 07991b2a0e | |||
| 51ea0c7a80 | |||
| 8b01428208 | |||
| f69e2617d4 | |||
| ddfdb47b93 | |||
| 14849f4211 | |||
| 067c599cb1 | |||
| 33ceaa38c0 | |||
| 0a0288c2ee | |||
| 90f888bc4b | |||
| 9bb34fb2a4 | |||
| 22b75a6b30 | |||
| 6af8410698 | |||
| f023c724fa | |||
| 6bbbe9cece | |||
| e6a4a04bb4 | |||
| 5c4a391d96 | |||
| a75b15d840 | |||
| 1322bd6326 | |||
| e2997c345c | |||
| 923a7e824f | |||
| 10ac0b5330 | |||
| bfeba9da40 | |||
| 752a865334 | |||
| 5eda771070 | |||
| 0826080f82 | |||
| 6fab785a49 | |||
| ecbcca5f14 | |||
| 861de56f10 | |||
| 3ce66e1201 | |||
| 7e8ea73363 | |||
| 9142525d21 | |||
| 62577ff1bb | |||
| 953556ccee | |||
| cc8621f304 | |||
| baae26d244 | |||
| cbf70cbeef | |||
| 1e28191e67 | |||
| ef4d9d38e7 | |||
| 52626406bf | |||
| fec6c77dc2 | |||
| 4db8ee7837 | |||
| 9bfde0e39d | |||
| cfac8493cd | |||
| 0d003a4f7e | |||
| 106a4759ae | |||
| 06d09220c5 | |||
| a19960b18f | |||
| 656dd7d884 | |||
| b965859537 | |||
| 707e96d2e9 | |||
| ab795674b2 | |||
| eb74feb659 | |||
| 4b4c49b5c7 | |||
| 1c134e02f9 | |||
| 85305f987f | |||
| ec6e2b8c7e | |||
| ab34435395 | |||
| da96d846eb | |||
| 9f6cb2658a | |||
| 33402012b6 | |||
| 97cf1651f9 | |||
| 97bfe69986 | |||
| b08fd7f433 | |||
| 83c6a690ec | |||
| cb54ae4dee | |||
| be2dcbf085 | |||
| 4df2726b95 | |||
| aa78e850bb | |||
| c05f18c12c | |||
| b4041a11a0 | |||
| bcf222a38b | |||
| 7cfe928fcc | |||
| b5ad7c12e3 | |||
| 6286c5736e | |||
| 0773d3ef0e | |||
| 7b912a9325 | |||
| 008155fa61 | |||
| 117cd9f568 | |||
| 3b14727f81 | |||
| 444a3733bd | |||
| de63ff9bb5 | |||
| 2b2a4c59a8 | |||
| 30f737bd7c | |||
| 1e2c6ca66f | |||
| 7b13716ce3 | |||
| 7a1b21a2ac | |||
| 2345bb6442 | |||
| 47fd9e1db3 | |||
| f8eee081b3 | |||
| 84e0647c35 | |||
| 60804dd98c | |||
| 5789fac985 | |||
| 2a89c6316f | |||
| 4c0a26a900 | |||
| 6915617980 | |||
| 0a09e14021 | |||
| 34446494a9 | |||
| 8477c2f63b | |||
| 105d1f7619 | |||
| a4be712bd7 | |||
| 3a9a65920e | |||
| ff62847758 | |||
| 0207f617c2 | |||
| 5c530952d3 | |||
| bdcefefd92 | |||
| 68cebfa68c | |||
| 94b337e772 | |||
| fd1f6223c0 | |||
| 0d86e461cd | |||
| 9f06748b63 | |||
| 9769fca6af | |||
| 416ef0991d | |||
| 2150a563eb | |||
| 1ef08c17ec | |||
| 15e375900c | |||
| d70d54d3f7 | |||
| 0159f639d2 | |||
| 19d03536b5 | |||
| 214a716f16 | |||
| 1f5ecedf9f | |||
| 3b52dab371 | |||
| 40340a0abd | |||
| c2311464f0 | |||
| f4f6fa2523 | |||
| 44d01eaec0 | |||
| 300ff04f2e | |||
| 6a34e9f706 | |||
| 97c8953dbd | |||
| 179d0bc567 | |||
| 2d1242f3c0 | |||
| b7f7e168b9 | |||
| 83244ec496 | |||
| eb3221fa4b | |||
| 4e37ca4c44 | |||
| ae6172068e | |||
| 65134c02af | |||
| fa9e1aae7c | |||
| 3d1401e1c2 | |||
| 8c102f5d5f | |||
| c4e1790444 | |||
| 68c1fa1216 | |||
| eb094be9af | |||
| 8527e9860d | |||
| ceb1ed1404 | |||
| 968fa63f51 | |||
| 2d530d204e | |||
| 65efde05ee | |||
| a5d261ac93 | |||
| b61cd3839a | |||
| 665567bf88 | |||
| a074a9fbed | |||
| 0e30954f6e | |||
| 0e98cffc64 | |||
| e7d5e766e3 | |||
| 88b0ef752c | |||
| 1a8fc9eab4 | |||
| 76dd8f73e4 | |||
| 4807c4babb | |||
| 9b4f5291f0 | |||
| 5edc4c55f5 | |||
| 563bf06b8e | |||
| cdaa2679d1 | |||
| 4efac6023f | |||
| c6e3806024 | |||
| e3df5b8e3b | |||
| 5afc0aebd5 | |||
| d23446b409 | |||
| bc63b457d3 | |||
| 30a8d62402 | |||
| 349683de24 | |||
| 06db7f48bd | |||
| 986a38d81d | |||
| 8b7ee75ca7 | |||
| f59bed777b | |||
| 87634f0df5 | |||
| bb618e74c9 | |||
| 92d2a7673b | |||
| 232fe43eb5 | |||
| d02cfe0557 | |||
| aca0b5635b | |||
| 4e8d3aaeb9 | |||
| 1c097b639a | |||
| ed80b3c82b | |||
| 1a59e174ae | |||
| bbca7c2e14 | |||
| 5c09490d5e | |||
| ba9b24a276 | |||
| 755b61efe3 | |||
| 75d6d94757 | |||
| a00285b38a | |||
| b529aa070a | |||
| 38d7b75831 | |||
| c3b81b5828 | |||
| 8aae753098 | |||
| 382a2aeabc | |||
| ec056ef3b3 | |||
| 96d3e6b620 | |||
| 85f59a3983 | |||
| 71035ab87b | |||
| bfd91af873 | |||
| de5d406362 | |||
| 8c8f405aa5 | |||
| a59f2e6f7c | |||
| 5d06ee1c41 | |||
| b818420df7 | |||
| dbe9c69771 | |||
| 3a58699f62 | |||
| f5804c3ce8 | |||
| 5ef7083ee6 | |||
| 25370ec412 | |||
| b55c25aa7c | |||
| 9f68ac8377 | |||
| 335cde4d4e | |||
| 878b8cf87c | |||
| 283904d976 | |||
| 11dea5182b | |||
| 87f8f49c39 | |||
| 595033b80f | |||
| ccd32ed7e6 | |||
| a40e3b24eb | |||
| 6a4ff9b0a7 | |||
| 2dfb0bf447 | |||
| 21e6352ace | |||
| f1089dbdf5 | |||
| e95efbff59 | |||
| 92b46baeed | |||
| d45fce6506 | |||
| e4374a6731 | |||
| cd402c4b86 | |||
| 25bf445582 | |||
| 74287289e0 | |||
| cc23f8565a | |||
| a537ced0d7 | |||
| 797d962e40 | |||
| 992be72d8d | |||
| 375712c0a8 | |||
| 0b7ab5923c | |||
| d077818508 | |||
| c991b75384 | |||
| 9f59f517b5 | |||
| 6654f78486 | |||
| 206fef682f | |||
| 3e454423a1 | |||
| 8326a9e923 | |||
| 5013830815 | |||
| e4ba53f460 | |||
| fabdc10467 | |||
| 91488c6a9f | |||
| f346364633 | |||
| 34e482e8a9 | |||
| 9aaf662a60 | |||
| 0b65ebbb00 | |||
| 74d1c88f82 | |||
| 70c8375810 | |||
| b554128337 | |||
| 4a8ce2ca18 | |||
| a2df22c1da | |||
| 047ceda252 | |||
| c94435360f | |||
| ed0b7a9740 | |||
| b168c4bb82 | |||
| 5078011f40 | |||
| 1428953cb7 | |||
| 7f962704d2 | |||
| 2970263db5 | |||
| 91d9fb90ac | |||
| adb28089b9 | |||
| 6f4a012ac4 | |||
| b807a41496 | |||
| 02f1fb797f | |||
| caee9117b3 | |||
| 48cec4c0f0 | |||
| d4df0c3562 | |||
| 8caab70b25 | |||
| 846f26db75 | |||
| ea2bdb226d | |||
| 0f93a3581f | |||
| 6607afc898 | |||
| 543b0ddefe | |||
| 6d191f04fb | |||
| 353a623e5f | |||
| e28f68aae3 | |||
| 5441592d68 | |||
| a8e96b142d | |||
| 4527b58084 | |||
| 80d14c1400 | |||
| 86ed26afeb | |||
| 35c4147cc1 | |||
| 711e69ddfb | |||
| 90d9cac19a | |||
| 61593d7d23 | |||
| 7c41e7adcc | |||
| 0215be8681 | |||
| 1139384ab3 | |||
| 45174422e9 | |||
| 2077d784fd | |||
| fa341101a8 | |||
| 1959ac589d | |||
| 5744493ac3 | |||
| 64eb4530af | |||
| d38fdfaced | |||
| f76484f824 | |||
| ef6adb0a27 | |||
| ca80df9541 | |||
| 828907120b | |||
| 460074b18a | |||
| 104bd4498e | |||
| 97082b1e55 | |||
| c9561fba71 | |||
| 8aa913fe09 | |||
| a598521491 | |||
| 6461456c16 | |||
| b4f697bdea | |||
| 721295b9d1 | |||
| 70c6e74623 | |||
| 1348ca27ab | |||
| 116592e436 | |||
| 19284a4ddf | |||
| 35e603b850 | |||
| 882c7bb35e | |||
| 9c8ce3a7f2 | |||
| 8a6af65786 | |||
| 4633bd902c | |||
| 10fcb8a2f6 | |||
| 3ad47eddfd | |||
| 636f1474bd | |||
| ebc9fac4dc | |||
| a0e5db6ed9 | |||
| f8acd8e222 | |||
| 7b21f82024 | |||
| 240cd7ce69 | |||
| 8a4f9c6f79 | |||
| 16f5865c4c | |||
| 25c3d8e6c1 | |||
| e1584c80a5 | |||
| 89dc493edf | |||
| aa59075939 | |||
| c55beaf30d | |||
| 67f1343744 | |||
| 6b0e0fc8e8 | |||
| af41ad5f53 | |||
| 622cd524e5 | |||
| 7266cf0d80 | |||
| 47fc11ed96 | |||
| fd927991c7 | |||
| 4b5e6d9856 | |||
| 3aa5ef9c95 | |||
| ded76b774c | |||
| 1bcf2f7410 | |||
| d8cce9ae11 | |||
| b4f29559cf | |||
| a7ea833860 | |||
| 9c54033343 | |||
| 7b9dab9d78 | |||
| 7f094cbafb | |||
| c4b8542b5b | |||
| 0345d37b2c | |||
| 97818c45f5 | |||
| 1a09c7311e | |||
| b658f6f707 | |||
| 1d63e4865b | |||
| 78fbd342d9 | |||
| 141c192017 | |||
| 87e44b83f7 | |||
| 1064831379 | |||
| 1e42ef7873 | |||
| c8327f3774 | |||
| 69cd4ff050 | |||
| 027f8bfd34 | |||
| 70ede22c18 | |||
| fd74298b0b | |||
| d47bae00ff | |||
| 0c91b1b598 | |||
| 6ba789c7cf | |||
| 54b70f6a1c | |||
| ac32a02ff4 | |||
| 993578277e | |||
| 48fb8c7d4c | |||
| 0f6f27aa15 | |||
| b2947e9ef6 | |||
| c5ef8a297b | |||
| 02ca6b8d47 | |||
| 98b92b4dfb | |||
| 1c2afdfa52 | |||
| 55b4bd9173 | |||
| e422c03ca5 | |||
| 831ef16a2f | |||
| 0deb0ed719 | |||
| f3d44ceda2 | |||
| cb11db6201 | |||
| b52f3d2617 | |||
| 39cd5bf1a2 | |||
| 19f03717bc | |||
| 6d728c25ae | |||
| c8daa2a06e | |||
| 4d41e679dd | |||
| acfebf9ff3 | |||
| 5af1b1e5a1 | |||
| bc9a4f00c1 | |||
| f13f3a155c | |||
| a19edb8aa7 | |||
| 6a0500acb2 | |||
| 96af69508f | |||
| 3fbc9764e9 | |||
| a79dcb76af | |||
| 86c2fd5df7 | |||
| 07eaeef133 | |||
| d1bd0a5667 | |||
| 49dad14c84 | |||
| 50e3c345d5 | |||
| a444dbb32a | |||
| 03e212bd37 | |||
| 0325eb13d5 | |||
| bfcb80aa64 | |||
| 91a6c0c6e4 | |||
| 845cc94513 | |||
| 21ccaceb53 | |||
| b9f63171f2 | |||
| 636d92d1a0 | |||
| 199ab9e00c | |||
| 77541764d1 | |||
| 53ae74f9c4 | |||
| 5bc1d6ba48 | |||
| 831ddcf84e |
@@ -1,5 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
echo "Switch to production database settings"
|
||||
exit
|
||||
|
||||
# abort on errors
|
||||
set -e
|
||||
|
||||
@@ -10,13 +13,13 @@ npm run build
|
||||
cd dist
|
||||
|
||||
# if you are deploying to a custom domain
|
||||
echo 'vera.hydev.org' > CNAME
|
||||
echo 'demo.vera.hydev.org' > CNAME
|
||||
|
||||
git init
|
||||
git add -A
|
||||
git commit -m 'deploy'
|
||||
|
||||
# if you are deploying to https://<USERNAME>.github.io/<REPO>
|
||||
git push -f git@github.com:HyDevelop/VeracrossAnalyzer.Client.git master:gh-pages
|
||||
git push -f git@github.com:Hykilpikonna/VeracrossAnalyzerDemo.git master:gh-pages
|
||||
|
||||
cd -
|
||||
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
@@ -8,24 +8,29 @@
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"core-js": "^2.6.5",
|
||||
"echarts": "^4.2.1",
|
||||
"element-ui": "^2.11.1",
|
||||
"@types/chroma-js": "^2.0.0",
|
||||
"@types/md5": "^2.2.0",
|
||||
"chroma-js": "^2.1.0",
|
||||
"core-js": "^2.6.10",
|
||||
"echarts": "^4.8.0",
|
||||
"element-ui": "^2.13.2",
|
||||
"md5": "^2.2.1",
|
||||
"moment": "^2.27.0",
|
||||
"p-wait-for": "^3.1.0",
|
||||
"v-charts": "^1.19.0",
|
||||
"vue": "^2.6.10",
|
||||
"vue-class-component": "^7.0.2",
|
||||
"vue-cookies": "^1.5.13",
|
||||
"vue-property-decorator": "^8.1.0"
|
||||
"vue": "^2.6.11",
|
||||
"vue-class-component": "^7.2.5",
|
||||
"vue-cookies": "^1.7.3",
|
||||
"vue-property-decorator": "^8.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "^3.10.0",
|
||||
"@vue/cli-plugin-typescript": "^3.10.0",
|
||||
"@vue/cli-service": "^3.10.0",
|
||||
"node-sass": "^4.9.0",
|
||||
"sass-loader": "^7.1.0",
|
||||
"typescript": "^3.4.3",
|
||||
"vue-template-compiler": "^2.6.10"
|
||||
"@vue/cli-plugin-babel": "^3.12.1",
|
||||
"@vue/cli-plugin-typescript": "^3.12.1",
|
||||
"@vue/cli-service": "^4.4.6",
|
||||
"node-sass": "^4.14.1",
|
||||
"sass-loader": "^8.0.2",
|
||||
"typescript": "^3.9.7",
|
||||
"vue-template-compiler": "^2.6.11"
|
||||
},
|
||||
"postcss": {
|
||||
"plugins": {
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>404: Redirecting...</title>
|
||||
<meta http-equiv = "refresh" content = "0; url = https://vera.hydev.org/" />
|
||||
</head>
|
||||
<body>
|
||||
404 Not Found! Redirecting to (<a href="https://vera.hydev.org">https://vera.hydev.org</a>)...
|
||||
<script>
|
||||
window.location.href = 'https://vera.hydev.org';
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
{"success":true,"data":{"assignments":[{"score_id":5593330,"id":322150,"assignment_id":322150,"assignment_type_id":3,"assignment_type":"Quiz","assignment_type_sort_key":3,"assignment_description":"2.3 Open Notes","grading_period":"Quarter 1","assignment_date_long":"Wed, Sep 11","due_date_long":"Wed, Sep 11","due_date":"Sep 11","due_day":"Wed","_date":"09/11/2019","include_in_calculated_grade":1,"num_attachments":0,"num_criteria":0,"num_feedback":0,"maximum_score":16,"points_possible":16,"raw_score":"14","percent_grade":"8750%","completion_status_id":3,"completion_status":"Complete","is_unread":1.0,"is_notification":0,"is_problem":0,"display_grade":1,"display_score":1,"display_maximum_score":1,"display_percent_grade":1,"display_points_possible":1,"allow_student_feedback":0},{"score_id":5584935,"id":321649,"assignment_id":321649,"assignment_type_id":3,"assignment_type":"Quiz","assignment_type_sort_key":3,"assignment_description":"2.2 Open Notes","grading_period":"Quarter 1","assignment_date_long":"Mon, Sep 09","due_date_long":"Mon, Sep 09","due_date":"Sep 09","due_day":"Mon","_date":"09/09/2019","include_in_calculated_grade":1,"num_attachments":0,"num_criteria":0,"num_feedback":0,"maximum_score":10,"points_possible":10,"raw_score":"6","percent_grade":"6000%","completion_status_id":3,"completion_status":"Complete","is_notification":0,"is_problem":0,"display_grade":1,"display_score":1,"display_maximum_score":1,"display_percent_grade":1,"display_points_possible":1,"allow_student_feedback":0},{"score_id":5602940,"id":322723,"assignment_id":322723,"assignment_type_id":2,"assignment_type":"Homework","assignment_type_sort_key":2,"assignment_description":"Autobiography","grading_period":"Quarter 1","assignment_date_long":"Wed, Sep 04","due_date_long":"Thu, Sep 05","due_date":"Sep 05","due_day":"Thu","_date":"09/05/2019","include_in_calculated_grade":1,"num_attachments":0,"num_criteria":0,"num_feedback":0,"maximum_score":10,"points_possible":10,"raw_score":"","percent_grade":"0%","completion_status_id":0,"completion_status":"Pending","is_notification":0,"is_problem":0,"display_grade":1,"display_score":1,"display_maximum_score":1,"display_percent_grade":1,"display_points_possible":1,"allow_student_feedback":0}],"attachments":[],"criteria":[],"criteria_grade_scale_levels":[]}}
|
||||
@@ -0,0 +1 @@
|
||||
{"success":true,"data":{"assignments":[],"attachments":[],"criteria":[],"criteria_grade_scale_levels":[]}}
|
||||
@@ -0,0 +1 @@
|
||||
{"success":true,"data":{"assignments":[],"attachments":[],"criteria":[],"criteria_grade_scale_levels":[]}}
|
||||
@@ -0,0 +1 @@
|
||||
{"success":true,"data":{"assignments":[],"attachments":[],"criteria":[],"criteria_grade_scale_levels":[]}}
|
||||
@@ -0,0 +1 @@
|
||||
{"success":true,"data":{"assignments":[],"attachments":[],"criteria":[],"criteria_grade_scale_levels":[]}}
|
||||
@@ -0,0 +1 @@
|
||||
{"success":true,"data":{"assignments":[],"attachments":[],"criteria":[],"criteria_grade_scale_levels":[]}}
|
||||
@@ -0,0 +1,250 @@
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"user": {
|
||||
"id": 1,
|
||||
"schoolPersonPk": 109467,
|
||||
"username": "ygui21",
|
||||
"lastLogin": "Jun 5, 2020 1:28:51 PM",
|
||||
"firstLogin": "Dec 2, 2019 7:31:15 PM",
|
||||
"firstName": "Yijie",
|
||||
"lastName": "Gui",
|
||||
"graduationYear": 2021,
|
||||
"emails": "ygui21@stjohnsprep.org",
|
||||
"classes": "32451|32453|32458|32856|32872|32874|32878|32880|32882|32890|33070|33093|33121|33173|33337|33464|34174|34197|34199|34209"
|
||||
},
|
||||
"token": "Removed",
|
||||
"courses": [
|
||||
{
|
||||
"level": "H",
|
||||
"id_ci": 196,
|
||||
"rating": {
|
||||
"id_ci": 196,
|
||||
"id_user": 1,
|
||||
"userFullName": "Yijie]\u003d[Gui",
|
||||
"ratings": [
|
||||
5,
|
||||
5,
|
||||
3,
|
||||
5,
|
||||
4
|
||||
],
|
||||
"comment": "Mr. Crowell\u0027s selected books for this course are very interesting, and the essay topics are generally unique too, allowing us to express our creativity. His lectures are also very in-depth, strengthening our understanding of the ideas that the authors wanted to express. However, the style of the classroom might be boring for some people. For grading fairness, I think it is very fair, but his standards are a little bit too high because achieving a perfect score for an essay is very much impossible."
|
||||
},
|
||||
"name": "English 3 H",
|
||||
"teacherName": "Mr. Crowell",
|
||||
"id": 33337,
|
||||
"assignmentsId": 10934147,
|
||||
"letterGrade": "A+",
|
||||
"numericGrade": 97.14,
|
||||
"status": "active"
|
||||
},
|
||||
{
|
||||
"level": "AP",
|
||||
"id_ci": 19,
|
||||
"rating": {
|
||||
"id_ci": 19,
|
||||
"id_user": -1,
|
||||
"userFullName": "Anonymous]\u003d[Student",
|
||||
"ratings": [
|
||||
5,
|
||||
5,
|
||||
5,
|
||||
5,
|
||||
5
|
||||
],
|
||||
"comment": "Calculus made so much easier"
|
||||
},
|
||||
"name": "AP Calculus AB (Juniors)",
|
||||
"teacherName": "Ms. Dobrowolski",
|
||||
"id": 32453,
|
||||
"assignmentsId": 10934142,
|
||||
"letterGrade": "A+",
|
||||
"numericGrade": 100.0,
|
||||
"status": "active"
|
||||
},
|
||||
{
|
||||
"level": "AP",
|
||||
"id_ci": 251,
|
||||
"rating": {
|
||||
"id_ci": 251,
|
||||
"id_user": 1,
|
||||
"userFullName": "Yijie]\u003d[Gui",
|
||||
"ratings": [
|
||||
5,
|
||||
5,
|
||||
5,
|
||||
5,
|
||||
5
|
||||
],
|
||||
"comment": "Cations go meow"
|
||||
},
|
||||
"name": "AP Chemistry",
|
||||
"teacherName": "Ms. Stone",
|
||||
"id": 33464,
|
||||
"assignmentsId": 10934148,
|
||||
"letterGrade": "A+",
|
||||
"numericGrade": 100.0,
|
||||
"status": "active"
|
||||
},
|
||||
{
|
||||
"level": "AP",
|
||||
"id_ci": 22,
|
||||
"rating": {
|
||||
"id_ci": 22,
|
||||
"id_user": 1,
|
||||
"userFullName": "Yijie]\u003d[Gui",
|
||||
"ratings": [
|
||||
5,
|
||||
5,
|
||||
5,
|
||||
3,
|
||||
4
|
||||
],
|
||||
"comment": "Mr. Dankert can\u0027t really systematically teach, often he forgets to tell us something very important and then forgets that he forgot. The labs explained a lot of them very well."
|
||||
},
|
||||
"name": "AP Physics 1",
|
||||
"teacherName": "Mr. Dankert",
|
||||
"id": 32458,
|
||||
"assignmentsId": 10934143,
|
||||
"letterGrade": "A",
|
||||
"numericGrade": 95.67,
|
||||
"status": "active"
|
||||
},
|
||||
{
|
||||
"level": "AP",
|
||||
"id_ci": 18,
|
||||
"rating": {
|
||||
"id_ci": 18,
|
||||
"id_user": 1,
|
||||
"userFullName": "Yijie]\u003d[Gui",
|
||||
"ratings": [
|
||||
5,
|
||||
5,
|
||||
5,
|
||||
5,
|
||||
5
|
||||
],
|
||||
"comment": "Psychology is the best, and most valuable class I\u0027ve ever taken! Everything is just so relatable to my life!"
|
||||
},
|
||||
"name": "AP Psychology",
|
||||
"teacherName": "Mr. Emerson",
|
||||
"id": 32451,
|
||||
"assignmentsId": 10934141,
|
||||
"letterGrade": "A+",
|
||||
"numericGrade": 100.0,
|
||||
"status": "active"
|
||||
},
|
||||
{
|
||||
"level": "A",
|
||||
"id_ci": 98,
|
||||
"rating": {
|
||||
"id_ci": 98,
|
||||
"id_user": 1,
|
||||
"userFullName": "Yijie]\u003d[Gui",
|
||||
"ratings": [
|
||||
5,
|
||||
5,
|
||||
5,
|
||||
5,
|
||||
5
|
||||
],
|
||||
"comment": "Mr. Pynchon is very nice and encouraging."
|
||||
},
|
||||
"name": "US History A",
|
||||
"teacherName": "Mr. Pynchon",
|
||||
"id": 33093,
|
||||
"assignmentsId": 10941280,
|
||||
"letterGrade": "A+",
|
||||
"numericGrade": 99.39,
|
||||
"status": "active"
|
||||
},
|
||||
{
|
||||
"level": "H",
|
||||
"id_ci": 100,
|
||||
"name": "US History H",
|
||||
"teacherName": "Ms. Heath",
|
||||
"id": 33096,
|
||||
"assignmentsId": 10934144,
|
||||
"status": "past"
|
||||
},
|
||||
{
|
||||
"level": "A",
|
||||
"id_ci": 134,
|
||||
"name": "Relational Dynamics A",
|
||||
"teacherName": "Mr. Reinbold",
|
||||
"id": 33173,
|
||||
"assignmentsId": 10934146,
|
||||
"status": "past"
|
||||
},
|
||||
{
|
||||
"level": "A",
|
||||
"id_ci": 77,
|
||||
"rating": {
|
||||
"id_ci": 77,
|
||||
"id_user": -1,
|
||||
"userFullName": "Anonymous]\u003d[Student",
|
||||
"ratings": [
|
||||
5,
|
||||
4,
|
||||
5,
|
||||
5,
|
||||
3
|
||||
],
|
||||
"comment": "Honestly, everything is great except that the grading is way too harsh for an Accelerated course."
|
||||
},
|
||||
"name": "Social Justice A",
|
||||
"teacherName": "Mr. Reinbold",
|
||||
"id": 33121,
|
||||
"assignmentsId": 10934145,
|
||||
"letterGrade": "A+",
|
||||
"numericGrade": 97.0,
|
||||
"status": "active"
|
||||
},
|
||||
{
|
||||
"level": "Sport",
|
||||
"name": "Yoga AS",
|
||||
"teacherName": "Ms. Fanikos",
|
||||
"id": 33070,
|
||||
"assignmentsId": 10935189,
|
||||
"status": "past"
|
||||
},
|
||||
{
|
||||
"level": "Club",
|
||||
"id_ci": 316,
|
||||
"name": "HS Magic Trick Club 2019",
|
||||
"teacherName": "Mr. Reinbold",
|
||||
"id": 34174,
|
||||
"assignmentsId": 10949139,
|
||||
"status": "active"
|
||||
},
|
||||
{
|
||||
"level": "Club",
|
||||
"id_ci": 331,
|
||||
"name": "HS Science \u0026amp; Technology 2019",
|
||||
"teacherName": "Ms. Erwin",
|
||||
"id": 34197,
|
||||
"assignmentsId": 10952100,
|
||||
"status": "active"
|
||||
},
|
||||
{
|
||||
"level": "Club",
|
||||
"id_ci": 333,
|
||||
"name": "HS Computer Club 2019",
|
||||
"teacherName": "Mr. Gilmore",
|
||||
"id": 34199,
|
||||
"assignmentsId": 10951448,
|
||||
"status": "active"
|
||||
},
|
||||
{
|
||||
"level": "Club",
|
||||
"id_ci": 336,
|
||||
"name": "HS Chinese Ambassadors Club 2019",
|
||||
"teacherName": "Mrs. Mills",
|
||||
"id": 34209,
|
||||
"assignmentsId": 10953979,
|
||||
"status": "active"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -23,6 +24,18 @@
|
||||
|
||||
<!-- 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 -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Q615K1KFLC"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', 'G-Q615K1KFLC');
|
||||
</script>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Redirecting...</title>
|
||||
<meta http-equiv = "refresh" content = "0; url = https://vera.hydev.org/#info" />
|
||||
</head>
|
||||
<body>
|
||||
Redirecting to (<a href="https://vera.hydev.org/#info">https://vera.hydev.org/#info</a>)...
|
||||
<script>
|
||||
window.location.href = 'https://vera.hydev.org/#info';
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 5.2 KiB |
@@ -1,2 +0,0 @@
|
||||
cd ../
|
||||
npm run serve
|
||||
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 44 KiB |
@@ -0,0 +1,45 @@
|
||||
import LoginUser from '@/logic/login-user';
|
||||
import App from '@/components/app/app';
|
||||
import pWaitFor from 'p-wait-for';
|
||||
|
||||
export default class AppDemo
|
||||
{
|
||||
static loadDemo(app: App)
|
||||
{
|
||||
app.logLoading('1. Logging in...')
|
||||
App.http.get('./demo-data/token.json').then(response =>
|
||||
{
|
||||
app.user = new LoginUser(response.data)
|
||||
app.courses = app.user.courses
|
||||
|
||||
app.logLoading('1. Loading assignments...')
|
||||
app.courses.forEach(course =>
|
||||
{
|
||||
App.http.get(`./demo-data/assignments-${course.assignmentsId}.json`).then(response =>
|
||||
{
|
||||
course.loadAssignments(response.data);
|
||||
})
|
||||
})
|
||||
|
||||
pWaitFor(() => app.courses.every(c => c.rawAssignments)).then(() =>
|
||||
{
|
||||
app.gradedCourses = app.courses.filter(c => c.isGraded)
|
||||
app.gradedCourses.forEach(c => [0, 1, 2, 3].forEach(i => c.termGrading[i] = {method: 'TOTAL_MEAN', weightingMap: {}}))
|
||||
app.logLoading('');
|
||||
app.assignmentsReady = true
|
||||
app.showRating = true
|
||||
|
||||
app.$alert(
|
||||
'This demo analyzes an offline snapshot of my data from Jun 6, 2020, ' +
|
||||
'which displays my academic results from Junior year.<br/>' +
|
||||
'<br/>' +
|
||||
'Feel free to click around! 😇<br/>' +
|
||||
'<br/>' +
|
||||
'-- The Veracross Analyzer Team (YGui)<br/>' +
|
||||
'-- Made with 🧡 in SJP',
|
||||
'🥳 Welcome to VeracrossAnalyzer Demo!',
|
||||
{dangerouslyUseHTMLString: true, confirmButtonText: 'OK'});
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,160 @@
|
||||
div
|
||||
{
|
||||
font-family: -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;
|
||||
}
|
||||
|
||||
#app
|
||||
{
|
||||
font-family: 'Avenir', Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-align: center;
|
||||
color: #2c3e50;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-align: center;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
#app-content
|
||||
{
|
||||
// Limit max width
|
||||
max-width: 1300px;
|
||||
text-align: center;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.theme-default
|
||||
{
|
||||
--unread: #ff6c00;
|
||||
--main: #0c6dad;
|
||||
|
||||
--assignment-type-2: #3f991e;
|
||||
--assignment-type-3: #ff9900;
|
||||
--assignment-type-4: #b02b02;
|
||||
}
|
||||
|
||||
.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
|
||||
#app-inner, #overall, #overall-course, .overall-span, #app-content
|
||||
{
|
||||
background: var(--dark-layer-1) !important;
|
||||
}
|
||||
|
||||
// Course card
|
||||
.entry-box, .none .unread-number {background: #797979 !important}
|
||||
.entry-box.max {background-color: #949494 !important}
|
||||
.entry-box.percent {background-color: #a7a490 !important}
|
||||
.course-name {color: #cffff6 !important}
|
||||
#block-grade #updates.none #unread-number {background: #757575 !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;
|
||||
}
|
||||
|
||||
// Nav bar
|
||||
.el-menu--horizontal>.el-menu-item.is-active {color: var(--dark-foreground) !important;}
|
||||
}
|
||||
|
||||
// ##############
|
||||
// # Global CSS #
|
||||
// ##############
|
||||
|
||||
.el-card
|
||||
{
|
||||
margin: 10px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.el-card.large
|
||||
{
|
||||
height: 494px;
|
||||
}
|
||||
|
||||
// Fix padding
|
||||
.el-card__body
|
||||
{
|
||||
padding-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
|
||||
// Vertical centering
|
||||
.vertical-center
|
||||
{
|
||||
// Vertical center
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
// Remove card padding for styling issues
|
||||
div.el-card.course-card > div.el-card__body
|
||||
{
|
||||
padding-right: 0 !important;
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
||||
// Clickable text
|
||||
.clickable:hover
|
||||
{
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
// Non-selectable text
|
||||
.unselectable
|
||||
{
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.el-dropdown-menu__item
|
||||
{
|
||||
font-family: Nunito Sans, Helvetica Neue, Microsoft YaHei, "微软雅黑", Arial, sans-serif;
|
||||
}
|
||||
|
||||
// Fix word breaking
|
||||
.el-dialog__body
|
||||
{
|
||||
word-break: unset !important;
|
||||
}
|
||||
|
||||
.comic
|
||||
{
|
||||
font-family: "Comic Sans MS", Nunito Sans, Helvetica Neue, Microsoft YaHei, "微软雅黑", Arial, sans-serif;
|
||||
}
|
||||
|
||||
#demo-not-available
|
||||
{
|
||||
padding-top: 40vh;
|
||||
font-size: 2em;
|
||||
color: #bbbbbb;
|
||||
margin: 0 50px;
|
||||
}
|
||||
|
||||
@@ -1,168 +1,147 @@
|
||||
import {Component, Vue} from 'vue-property-decorator';
|
||||
import Login from '@/components/login/login';
|
||||
import Navigation from '@/components/navigation/navigation';
|
||||
import Overall from '@/pages/overall/overall';
|
||||
import Login from '@/components/login/login.vue';
|
||||
import Navigation from '@/components/navigation/navigation.vue';
|
||||
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';
|
||||
|
||||
/**
|
||||
* Objects of this interface represent assignment grades.
|
||||
*/
|
||||
export interface Grade
|
||||
{
|
||||
type: string,
|
||||
description: string,
|
||||
date: string,
|
||||
complete: string,
|
||||
include: boolean,
|
||||
display: boolean,
|
||||
|
||||
scoreMax: number,
|
||||
score: number
|
||||
}
|
||||
|
||||
/**
|
||||
* A course
|
||||
*/
|
||||
export interface Course
|
||||
{
|
||||
assignmentsId: number,
|
||||
id: number,
|
||||
name: string,
|
||||
teacherName: string,
|
||||
status: string,
|
||||
|
||||
letterGrade?: string,
|
||||
numericGrade?: number,
|
||||
|
||||
level: string,
|
||||
scaleUp: number,
|
||||
|
||||
grading:
|
||||
{
|
||||
method: string,
|
||||
weightingMap: {[index: string]: number}
|
||||
}
|
||||
|
||||
assignments: Grade[]
|
||||
}
|
||||
import {HttpUtils} from '@/logic/utils/http-utils';
|
||||
import Loading from '@/components/overlays/loading.vue';
|
||||
import CoursePage from '@/pages/course/course-page.vue';
|
||||
import Course from '@/logic/course';
|
||||
import LoginUser from '@/logic/login-user';
|
||||
import NavController from '@/logic/nav-controller';
|
||||
import Info from '@/statics/Info.vue';
|
||||
import CourseSelection from '@/pages/course-selection/course-selection.vue';
|
||||
import AppDemo from '@/components/app/app-demo';
|
||||
|
||||
@Component({
|
||||
components: {Login, Navigation, Overall},
|
||||
components: {Login, Navigation, Overall, Loading, CoursePage, Info, CourseSelection},
|
||||
})
|
||||
export default class App extends Vue
|
||||
{
|
||||
// Is the login panel shown
|
||||
public showLogin: boolean = true;
|
||||
|
||||
// List of course that the student takes
|
||||
public courses: Course[] = [];
|
||||
|
||||
// List of course that should be displayed
|
||||
public filteredCourses: Course[] = [];
|
||||
|
||||
// Currently selected tab
|
||||
public selectedTab: string = 'overall';
|
||||
courses: Course[] = [];
|
||||
gradedCourses: Course[] = [];
|
||||
|
||||
// Are the course assignments loaded from the server.
|
||||
public assignmentsReady: boolean = false;
|
||||
assignmentsReady: boolean = false;
|
||||
|
||||
// Token
|
||||
public token: string = '';
|
||||
user: LoginUser = null as any;
|
||||
|
||||
// Loading text
|
||||
loading: string = '';
|
||||
|
||||
// Loading error
|
||||
loadingError: boolean = false;
|
||||
|
||||
// Navigation controller
|
||||
nav: NavController = new NavController();
|
||||
|
||||
// Http Client
|
||||
public http: HttpUtils = new HttpUtils('');
|
||||
static http: HttpUtils = new HttpUtils();
|
||||
|
||||
// Instance
|
||||
static instance: App;
|
||||
|
||||
// Static page
|
||||
staticPage: string = '';
|
||||
|
||||
// Dark mode
|
||||
darkMode: boolean = this.$cookies.isKey('dark');
|
||||
|
||||
// Show rating
|
||||
showRating: boolean = this.$cookies.get('show-rating') == 'set=yes';
|
||||
|
||||
// Demo mode
|
||||
demoMode: boolean = window.location.hostname == 'demo.vera.hydev.org' || this.$cookies.isKey('demo-mode')
|
||||
|
||||
// Is the login panel shown
|
||||
showLogin: boolean = !this.demoMode
|
||||
|
||||
/**
|
||||
* This is called when the instance is created.
|
||||
*/
|
||||
public created()
|
||||
created()
|
||||
{
|
||||
// Show splash
|
||||
console.log(Constants.SPLASH);
|
||||
|
||||
// Update instance
|
||||
App.instance = this;
|
||||
|
||||
// Check location
|
||||
if (window.location.hash == '#info')
|
||||
{
|
||||
this.staticPage = 'info';
|
||||
}
|
||||
|
||||
// Default config
|
||||
if (!this.$cookies.isKey('show-rating'))
|
||||
{
|
||||
this.showRating = Constants.CURRENT_TERM == 3;
|
||||
}
|
||||
|
||||
// Demo
|
||||
if (this.demoMode)
|
||||
{
|
||||
AppDemo.loadDemo(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is called when the user logs in.
|
||||
*
|
||||
* @param token Authorization token
|
||||
* @param user Authorization user
|
||||
*/
|
||||
public onLogin(token: string)
|
||||
onLogin(user: LoginUser)
|
||||
{
|
||||
// Hide login bar
|
||||
this.showLogin = false;
|
||||
|
||||
// Store token
|
||||
this.token = token;
|
||||
// Show loading message
|
||||
this.logLoading('1. Logging in...');
|
||||
|
||||
// Assign token to http client
|
||||
this.http.token = token;
|
||||
// Store user
|
||||
this.user = user;
|
||||
this.courses = user.courses
|
||||
|
||||
// Load data
|
||||
this.loadCoursesAfterLogin();
|
||||
}
|
||||
// Assign user to http client
|
||||
App.http.user = user;
|
||||
|
||||
/**
|
||||
* Load courses data after login.
|
||||
*/
|
||||
public loadCoursesAfterLogin()
|
||||
{
|
||||
this.http.post('/courses', {}).then(response =>
|
||||
{
|
||||
// Check success
|
||||
if (response.success)
|
||||
{
|
||||
// Save courses
|
||||
this.courses = response.data;
|
||||
|
||||
// Load assignments
|
||||
this.loadAssignments();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Show error message TODO: Show it properly
|
||||
alert(response.data);
|
||||
}
|
||||
})
|
||||
.catch(alert);
|
||||
// Load assignments
|
||||
this.loadAssignments();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the assignments of the courses
|
||||
*/
|
||||
public loadAssignments()
|
||||
loadAssignments()
|
||||
{
|
||||
// Show loading message
|
||||
this.logLoading('1. Loading assignments...');
|
||||
|
||||
// Get assignments for all the courses
|
||||
this.courses.forEach(course =>
|
||||
{
|
||||
// Send request to get assignments
|
||||
this.http.post('/assignments', {id: course.assignmentsId}).then(response =>
|
||||
App.http.post('/assignments', {'assignmentsId': course.assignmentsId}).then(response =>
|
||||
{
|
||||
// Check success
|
||||
if (response.success)
|
||||
{
|
||||
// Load assignments
|
||||
// Parse json and filter it
|
||||
course.assignments = JsonUtils.filterAssignments(response.data);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Show error message TODO: Show it properly
|
||||
alert(response.data);
|
||||
course.loadAssignments(response.data);
|
||||
}
|
||||
else throw new Error(response.data);
|
||||
})
|
||||
.catch(alert);
|
||||
.catch(e => this.showError(`Error: Assignments data failed to load.\n(${e})`));
|
||||
});
|
||||
|
||||
// Wait for assignments to be ready.
|
||||
pWaitFor(() => this.isAssignmentsReady()).then(() =>
|
||||
pWaitFor(() => this.courses.every(c => c.rawAssignments != null)).then(() =>
|
||||
{
|
||||
// Filter courses
|
||||
this.filteredCourses = CourseUtils.getGradedCourses(this.courses);
|
||||
this.gradedCourses = this.courses.filter(c => c.isGraded);
|
||||
|
||||
// Check grading algorithms
|
||||
this.checkGradingAlgorithms();
|
||||
@@ -170,99 +149,110 @@ export default class App extends Vue
|
||||
}
|
||||
|
||||
/**
|
||||
* Are assignments ready or not
|
||||
*
|
||||
* @returns boolean Ready or not
|
||||
* Check the courses' grading algorithms. (Total-mean or percent-type)
|
||||
*/
|
||||
private isAssignmentsReady(): boolean
|
||||
checkGradingAlgorithms()
|
||||
{
|
||||
for (const course of this.courses)
|
||||
{
|
||||
if (course.assignments == null) return false;
|
||||
}
|
||||
// Show loading message
|
||||
this.logLoading('2. Checking grading algorithms...');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the courses' grading algorithms. (Total-average or percent-type)
|
||||
*/
|
||||
private checkGradingAlgorithms()
|
||||
{
|
||||
// Loop through all the courses
|
||||
for (const course of this.filteredCourses)
|
||||
for (const course of this.gradedCourses)
|
||||
{
|
||||
// Check if total-average grade is the same with percent-type grade
|
||||
if (course.numericGrade == GPAUtils.getTotalMeanAverage(course))
|
||||
for (const i of [0, 1, 2, 3])
|
||||
{
|
||||
course.grading = {method: 'TOTAL_AVERAGE', weightingMap: {}};
|
||||
}
|
||||
else
|
||||
{
|
||||
// Request grading scheme for this course
|
||||
this.http.post('/grading', {'assignment_id': course.assignmentsId}).then(response =>
|
||||
const cookieIndex = `va.grading.${i}.${course.assignmentsId}`;
|
||||
|
||||
// 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;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Show error message TODO: Show it properly
|
||||
alert(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', '3d');
|
||||
}
|
||||
}
|
||||
else throw new Error(resp.data);
|
||||
})
|
||||
.catch(alert)
|
||||
.catch(e => this.showError(`Error: Grading data failed to load.\n(${e})`))
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for done
|
||||
pWaitFor(() => this.isGradingReady()).then(() =>
|
||||
pWaitFor(() => this.gradedCourses.every(c => c.termGrading.every(g => g != null))).then(() =>
|
||||
{
|
||||
// When the assignments are ready
|
||||
// TODO: Display loading
|
||||
this.assignmentsReady = true;
|
||||
|
||||
// Remove loading
|
||||
this.logLoading('');
|
||||
|
||||
// Check if rating notification should be displayed
|
||||
if (this.courses.filter(c => c.rated).length == 0 && this.showRating &&
|
||||
!this.$cookies.isKey('rating-notified'))
|
||||
{
|
||||
// Show notification
|
||||
this.$cookies.set('rating-notified', true);
|
||||
this.showUpdates()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Are grading algorithms ready or not.
|
||||
*
|
||||
* @returns boolean Ready or not
|
||||
*/
|
||||
private isGradingReady(): boolean
|
||||
showUpdates()
|
||||
{
|
||||
for (const course of this.filteredCourses)
|
||||
{
|
||||
if (course.grading == undefined)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
this.$alert(
|
||||
'<b>TL;DR:</b><br/>' +
|
||||
'📅 Added a Course Selection tab to help you schedule for next year!<br/>' +
|
||||
'🤩 You can now give star ratings to your courses!<br/>' +
|
||||
'😮 You can also see others\' ratings in the course selection tab!<br/>' +
|
||||
'<br/>' +
|
||||
'That\'s it, try things out and have fun! 😇<br/>' +
|
||||
'<br/>' +
|
||||
'-- The Veracross Analyzer Team<br/>' +
|
||||
'-- Made with 🧡 in SJP',
|
||||
'🥳 Huge updates!',
|
||||
{dangerouslyUseHTMLString: true, confirmButtonText: 'OK', customClass: 'comic'});
|
||||
}
|
||||
|
||||
/**
|
||||
* This is called when a navigation tab is clicked
|
||||
* Log a message to loading screen
|
||||
*
|
||||
* @param tab Tab name
|
||||
* @param message Message
|
||||
*/
|
||||
public onNavigate(tab: string)
|
||||
logLoading(message: string)
|
||||
{
|
||||
// Debug output TODO: Remove this
|
||||
console.log(tab);
|
||||
if (message == '') this.loading = '';
|
||||
else this.loading += '\n' + message;
|
||||
}
|
||||
|
||||
// Update selected tab
|
||||
this.selectedTab = tab;
|
||||
/**
|
||||
* Show error message on loading screen
|
||||
*
|
||||
* @param message Error message
|
||||
*/
|
||||
showError(message: string)
|
||||
{
|
||||
this.loadingError = true;
|
||||
this.loading = message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign out
|
||||
*/
|
||||
public signOut()
|
||||
signOut()
|
||||
{
|
||||
// Clear all cookies
|
||||
this.$cookies.keys().forEach(key => this.$cookies.remove(key));
|
||||
@@ -270,4 +260,15 @@ export default class App extends Vue
|
||||
// Refresh
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* Select time (Eg. Term 1, Term 2, All Year, etc.)
|
||||
*
|
||||
* @param code
|
||||
*/
|
||||
selectTime(code: number)
|
||||
{
|
||||
// TODO: Optimize
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,34 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<login v-if="showLogin" v-on:login:token="onLogin" :http="http"></login>
|
||||
<navigation :courses="filteredCourses"
|
||||
v-on:sign-out="signOut()"
|
||||
v-on:navigation:select="onNavigate">
|
||||
</navigation>
|
||||
<div id="app" class="theme-default">
|
||||
<div id="app-inner" v-if="staticPage === ''" :class="{dark: darkMode, padding: nav.id !== 'course-selection'}">
|
||||
<login v-if="showLogin" v-on:login:user="onLogin"/>
|
||||
<navigation v-if="user != null"
|
||||
:app="this" :user="user" :nav="nav"
|
||||
@sign-out="signOut" @select-time="selectTime">
|
||||
</navigation>
|
||||
|
||||
<div id="app-content">
|
||||
<overall :courses="filteredCourses"
|
||||
v-if="selectedTab === 'overall' && assignmentsReady">
|
||||
</overall>
|
||||
<div id="app-content" v-if="assignmentsReady && loading === ''">
|
||||
<overall v-if="nav.id === 'overall'" :courses="gradedCourses"></overall>
|
||||
<course-page v-if="nav.id === 'course'" :course="gradedCourses.find(c => +c.id === +nav.info.id)"></course-page>
|
||||
<course-selection v-if="nav.id === 'course-selection' && !demoMode" :app="this"></course-selection>
|
||||
<div id="demo-not-available" class="unselectable" v-if="nav.id === 'course-selection' && demoMode">
|
||||
Course selection page is not available in demo mode.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<loading v-if="loading !== ''" :text="loading" :error="loadingError"/>
|
||||
</div>
|
||||
|
||||
<Info v-if="staticPage === 'info'"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./app.ts" lang="ts"></script>
|
||||
<style src="./app.scss" lang="scss"></style>
|
||||
<style src="./app.scss" lang="scss"/>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.padding
|
||||
{
|
||||
padding-bottom: 100px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<div class="el-loading-spinner" :class="{'not-centered': !centered}">
|
||||
<svg viewBox="25 25 50 50" class="circular" :style="{width: size + 'px', height: size + 'px'}">
|
||||
<circle cx="50" cy="50" r="20" fill="none" class="path"/>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator'
|
||||
|
||||
@Component
|
||||
export default class LoadingSpinner extends Vue
|
||||
{
|
||||
@Prop({default: '42'}) size: string
|
||||
@Prop({default: true}) centered: string
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.not-centered
|
||||
{
|
||||
top: unset;
|
||||
margin-top: unset;
|
||||
width: unset;
|
||||
text-align: unset;
|
||||
position: unset;
|
||||
}
|
||||
</style>
|
||||
@@ -5,6 +5,13 @@
|
||||
|
||||
}
|
||||
|
||||
// Logo image
|
||||
#login-logo-image
|
||||
{
|
||||
width: 80%;
|
||||
margin-bottom: -15px;
|
||||
}
|
||||
|
||||
// Parent overlay
|
||||
.login-overlay
|
||||
{
|
||||
|
||||
@@ -1,82 +1,130 @@
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import {Component, Vue} from 'vue-property-decorator';
|
||||
import Constants from '@/constants';
|
||||
import {HttpUtils} from '@/utils/http-utils';
|
||||
import App from '@/components/app/app';
|
||||
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.
|
||||
* TODO: Press enter to login
|
||||
*/
|
||||
@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 = '';
|
||||
|
||||
@Prop()
|
||||
public http?: HttpUtils;
|
||||
disableInput = false;
|
||||
|
||||
maintenance = '';
|
||||
|
||||
/**
|
||||
* This is called when the instance is created.
|
||||
*/
|
||||
public created()
|
||||
created()
|
||||
{
|
||||
// TODO: Check maintenance
|
||||
|
||||
// Check login cookies
|
||||
if (this.$cookies.isKey('va.token'))
|
||||
{
|
||||
// Already contains valid token / TODO: Validate
|
||||
this.$emit('login:token', this.$cookies.get('va.token'));
|
||||
// Check cookies version
|
||||
if (this.needToUpdateCookies()) this.clearCookies();
|
||||
else
|
||||
{
|
||||
// Login with token
|
||||
this.login('/login/token', {token: this.$cookies.get('va.token')});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On click, sends username and password to the server.
|
||||
* Check version number
|
||||
*
|
||||
* @returns boolean Need to clear cookies or not
|
||||
*/
|
||||
public onLoginClick()
|
||||
needToUpdateCookies(): boolean
|
||||
{
|
||||
// Make login button loading
|
||||
this.loading = true;
|
||||
// Version doesn't exist
|
||||
if (!this.$cookies.isKey('va.version')) return true;
|
||||
|
||||
// Bug
|
||||
if (this.$cookies.get('va.token') == 'undefined') return true
|
||||
|
||||
// If the current version is less than the min supported version
|
||||
return VersionUtils.compare(this.$cookies.get('va.version'), Constants.MIN_SUPPORTED_VERSION) == -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* When the user clicks, post the login request and process the response
|
||||
* This is also called when the user hits enter on the input boxes.
|
||||
*/
|
||||
loginClick()
|
||||
{
|
||||
// Simple checks
|
||||
if (this.username == '')
|
||||
{
|
||||
this.error = 'Username cannot be blank 🤔';
|
||||
}
|
||||
|
||||
// Format it
|
||||
this.username = this.username.toLowerCase().replace(/ /g, '').replace(/@.*/g, '');
|
||||
|
||||
// Actually login
|
||||
this.login('/login', {username: this.username, password: this.password})
|
||||
}
|
||||
|
||||
/**
|
||||
* Actually post the request and process the response
|
||||
*/
|
||||
login(url: string, data: any)
|
||||
{
|
||||
// Show loading
|
||||
this.disableInput = this.loading = true;
|
||||
|
||||
// Fetch request
|
||||
(<HttpUtils> this.http).post('/login', {username: this.username, password: this.password})
|
||||
.then(response =>
|
||||
App.http.post(url, data).then(response =>
|
||||
{
|
||||
// Check success
|
||||
if (response.success)
|
||||
{
|
||||
// Save token to cookies
|
||||
this.$cookies.set('va.token', response.data, '7d');
|
||||
this.$cookies.set('va.token', response.data.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));
|
||||
}
|
||||
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
|
||||
// TODO: Automatic report error
|
||||
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.
|
||||
* Clear cookies
|
||||
*/
|
||||
public onEnter()
|
||||
clearCookies()
|
||||
{
|
||||
this.onLoginClick();
|
||||
this.$cookies.keys().forEach(key => this.$cookies.remove(key));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,30 +2,35 @@
|
||||
<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="SJP Username (Eg. flast21)"
|
||||
:class="{'input-error': error !== ''}"
|
||||
v-if="!disableInput"
|
||||
@keyup.enter.native="loginClick">
|
||||
</el-input>
|
||||
|
||||
<el-input v-model="username"
|
||||
placeholder="School Username"
|
||||
:class="{'input-error': error !== ''}"
|
||||
@keyup.enter.native="onEnter">
|
||||
</el-input>
|
||||
<el-input v-model="password"
|
||||
placeholder="SJP Password"
|
||||
show-password=""
|
||||
:class="{'input-error': error !== ''}"
|
||||
v-if="!disableInput"
|
||||
@keyup.enter.native="loginClick">
|
||||
</el-input>
|
||||
|
||||
<el-input v-model="password"
|
||||
placeholder="Veracross Password"
|
||||
show-password=""
|
||||
:class="{'input-error': error !== ''}"
|
||||
@keyup.enter.native="onEnter">
|
||||
</el-input>
|
||||
<div class="el-form-item__error custom">{{error}}</div>
|
||||
|
||||
<div class="el-form-item__error custom">{{error}}</div>
|
||||
|
||||
<el-button plain type="primary" @click="onLoginClick" :loading="loading">Login</el-button>
|
||||
<el-button plain type="primary" @click="loginClick" :loading="loading">Login</el-button>
|
||||
</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"/>
|
||||
|
||||
@@ -15,3 +15,140 @@
|
||||
border-bottom-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
#nav-avatar
|
||||
{
|
||||
position: absolute;
|
||||
right: 0;
|
||||
margin: 10px 20px;
|
||||
}
|
||||
|
||||
#sign-out-button
|
||||
{
|
||||
// Float right
|
||||
position: absolute;
|
||||
right: 0;
|
||||
|
||||
// Set width and height
|
||||
height: 60px;
|
||||
width: 110px;
|
||||
}
|
||||
|
||||
#nav-grading-period
|
||||
{
|
||||
// Float right
|
||||
position: absolute;
|
||||
right: 80px;
|
||||
|
||||
// Margins
|
||||
margin-top: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
#nav-title
|
||||
{
|
||||
// Float left
|
||||
position: absolute;
|
||||
left: 0;
|
||||
|
||||
// Set height
|
||||
height: 60px;
|
||||
|
||||
// Center text
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
// Margins
|
||||
margin-left: 20px;
|
||||
margin-right: 8px;
|
||||
|
||||
// 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
|
||||
{
|
||||
// Down center
|
||||
width: 50%;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 25%;
|
||||
padding-top: 2px;
|
||||
box-shadow: 0 -2px 9px 0 #00000029;
|
||||
}
|
||||
|
||||
footer
|
||||
{
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#prev-course
|
||||
{
|
||||
// Up center
|
||||
width: 50%;
|
||||
position: absolute;
|
||||
top: 61px;
|
||||
left: 25%;
|
||||
padding-bottom: 2px;
|
||||
box-shadow: 0 2px 9px 0 #00000029;
|
||||
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
.nav-course-operations
|
||||
{
|
||||
// Background
|
||||
background-color: rgba(214, 214, 214, 0.67);
|
||||
opacity: 0.85;
|
||||
|
||||
// Font
|
||||
font-size: 14px;
|
||||
color: #ab8585;
|
||||
|
||||
// Cursor
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.el-submenu__title
|
||||
{
|
||||
padding-right: 5px !important;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,45 @@
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import Course from '@/logic/course';
|
||||
import Constants from '@/constants';
|
||||
import LoginUser from '@/logic/login-user';
|
||||
import NavController from '@/logic/nav-controller';
|
||||
import App from '@/components/app/app.ts';
|
||||
|
||||
/**
|
||||
* This component is the top navigation bar
|
||||
*/
|
||||
@Component({
|
||||
components: {},
|
||||
})
|
||||
@Component
|
||||
export default class Navigation extends Vue
|
||||
{
|
||||
public activeIndex: string = 'overall';
|
||||
@Prop({required: true}) app: App;
|
||||
@Prop({required: true}) nav: NavController;
|
||||
@Prop({required: true}) user: LoginUser;
|
||||
|
||||
@Prop() courses: any;
|
||||
private gradingPeriod: string = 'All Year';
|
||||
|
||||
// Instance
|
||||
static instance: Navigation;
|
||||
|
||||
/**
|
||||
* This is called when the instance is created.
|
||||
*/
|
||||
created()
|
||||
{
|
||||
// Check selected time
|
||||
if (!this.$cookies.isKey('va.grading-period'))
|
||||
{
|
||||
this.$cookies.set('va.grading-period', this.gradingPeriod, '10y');
|
||||
}
|
||||
this.gradingPeriod = this.$cookies.get('va.grading-period');
|
||||
}
|
||||
|
||||
/**
|
||||
* This is called when the instance is loaded.
|
||||
*/
|
||||
mounted()
|
||||
{
|
||||
Navigation.instance = this;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is called when the selection changes.
|
||||
@@ -19,21 +47,119 @@ 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.activeIndex = index;
|
||||
|
||||
// Call custom event
|
||||
this.$emit('navigation:select', this.activeIndex);
|
||||
try
|
||||
{
|
||||
// Is json
|
||||
this.nav.updateIndex(JSON.parse(index))
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
// Not json
|
||||
this.nav.updateIndex(index);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is called when the sign out button is clicked.
|
||||
* Move to the next course
|
||||
*
|
||||
* @param indexOffset Index offset (Eg. 1 for next)
|
||||
*/
|
||||
public signOut()
|
||||
nextCourse(indexOffset: number)
|
||||
{
|
||||
// Call custom event
|
||||
this.$emit('sign-out');
|
||||
// Set tab to the next index
|
||||
this.nav.updateIndex(this.findNextCourse(indexOffset).urlIndex)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the next course
|
||||
*
|
||||
* @param indexOffset Index offset (Eg. 1 for next)
|
||||
*/
|
||||
findNextCourse(indexOffset: number)
|
||||
{
|
||||
return this.findCourse(this.nav.info.id, indexOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find course
|
||||
*
|
||||
* @param courseId Course ID
|
||||
* @param indexOffset Index offset (Eg. 1 for next)
|
||||
*/
|
||||
findCourse(courseId: string, indexOffset: number)
|
||||
{
|
||||
// Find current course index
|
||||
let courseIndex = this.app.gradedCourses.findIndex(c => c.id == +courseId);
|
||||
|
||||
// Find next course
|
||||
return this.app.gradedCourses[courseIndex + indexOffset];
|
||||
}
|
||||
|
||||
/**
|
||||
* Select grading period
|
||||
*
|
||||
* @param command Term 1, Term 2, All Year, etc.
|
||||
*/
|
||||
selectGradingPeriod(command: string)
|
||||
{
|
||||
this.gradingPeriod = command;
|
||||
this.$cookies.set('va.grading-period', command, '10y');
|
||||
|
||||
// Call event
|
||||
this.$emit('select-time', this.getSelectedTerm());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get code for selected time
|
||||
*/
|
||||
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
|
||||
}
|
||||
case 'switch-dark':
|
||||
{
|
||||
this.app.darkMode = !this.app.darkMode;
|
||||
|
||||
if (this.app.darkMode) this.$cookies.set('dark', true);
|
||||
else this.$cookies.remove('dark');
|
||||
|
||||
break
|
||||
}
|
||||
case 'switch-rating':
|
||||
{
|
||||
this.app.showRating = !this.app.showRating;
|
||||
|
||||
if (this.app.showRating) this.$cookies.set('show-rating', 'set=yes', '30d');
|
||||
else this.$cookies.set('show-rating', 'set=no', '30d');
|
||||
|
||||
break
|
||||
}
|
||||
case 'updates':
|
||||
{
|
||||
this.app.showUpdates()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get version() {return Constants.VERSION}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,78 @@
|
||||
<template>
|
||||
<div id="navigation">
|
||||
<el-menu class="centered" :default-active="activeIndex" mode="horizontal" @select="onSelect">
|
||||
<el-menu style="margin-bottom: 10px;" class="centered" mode="horizontal"
|
||||
:default-active="nav.id" @select="onSelect">
|
||||
|
||||
<div id="nav-title">
|
||||
<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>
|
||||
|
||||
<el-submenu index="courses">
|
||||
<el-submenu index="">
|
||||
<template slot="title">Courses</template>
|
||||
<el-menu-item v-for="course in courses"
|
||||
:index="`course-${course.name}`"
|
||||
:key="course.name">{{course.name}}</el-menu-item>
|
||||
<el-menu-item v-for="course in app.gradedCourses"
|
||||
:index="JSON.stringify(course.urlIndex)"
|
||||
:key="course.id">{{course.name}}</el-menu-item>
|
||||
</el-submenu>
|
||||
|
||||
<el-button @click="signOut" id="sign-out-button" type="text">Sign Out</el-button>
|
||||
<el-menu-item index="course-selection">Course Selection</el-menu-item>
|
||||
|
||||
<!-- 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"/>
|
||||
</el-button>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item command="Term 1">Term 1</el-dropdown-item>
|
||||
<el-dropdown-item command="Term 2">Term 2</el-dropdown-item>
|
||||
<el-dropdown-item command="Term 3">Term 3</el-dropdown-item>
|
||||
<el-dropdown-item command="Term 4">Term 4</el-dropdown-item>
|
||||
<!-- TODO: Auto enable / disable quarters -->
|
||||
<el-dropdown-item command="All Year" divided>All Year</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
|
||||
<!-- 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.firstName}}</el-dropdown-item>
|
||||
|
||||
<el-dropdown-item icon="el-icon-sunrise" command="switch-dark" divided>
|
||||
{{!app.darkMode ? 'Dark Mode (Unfinished)' : 'Light Mode'}}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item icon="el-icon-edit-outline" command="switch-rating">
|
||||
{{app.showRating ? 'Hide rating buttons' : 'Show rating button'}}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item icon="el-icon-cold-drink" command="updates">
|
||||
Check out the updates
|
||||
</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>
|
||||
<div class="line"></div>
|
||||
|
||||
<!-- Previous course / Next course (Only when the page is courses) -->
|
||||
<div v-if="nav.id === 'course' && findNextCourse(-1) != null"
|
||||
@click="nextCourse(-1)" id="prev-course" class="nav-course-operations unselectable">
|
||||
▲ PREVIOUS COURSE ▲
|
||||
</div>
|
||||
<footer>
|
||||
<div v-if="nav.id === 'course' && findNextCourse(1) != null"
|
||||
@click="nextCourse(1)" id="next-course" class="nav-course-operations unselectable">
|
||||
▼ 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"/>
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<div id="loading">
|
||||
<div id="text" :class="message">
|
||||
{{message}}
|
||||
|
||||
<div v-if="!error" class="el-loading-spinner">
|
||||
<svg viewBox="25 25 50 50" class="circular">
|
||||
<circle cx="50" cy="50" r="20" fill="none" class="path" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div v-if="error" id="error-details">
|
||||
<span v-for="(line, index) in split" :style="`font-size: ${-index === 0 ? 16 : 12}px;`">
|
||||
{{line}}
|
||||
<br>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!error" id="details">
|
||||
<span v-for="(line, index) in split" :style="`font-size: ${16 - split.length + index}px;`">
|
||||
{{line}}
|
||||
<br>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
|
||||
@Component
|
||||
export default class Loading extends Vue
|
||||
{
|
||||
@Prop({required: true}) text: string;
|
||||
|
||||
@Prop({required: true}) error: boolean;
|
||||
|
||||
get split()
|
||||
{
|
||||
return this.text.split('\n');
|
||||
}
|
||||
|
||||
get message()
|
||||
{
|
||||
return this.error ? 'Error' : 'Loading';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#loading
|
||||
{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
box-shadow: inset 0 0 1px 1px rgba(0,0,0,.1);
|
||||
background: -webkit-linear-gradient(left, rgba(95, 18, 72, 0.4), rgba(42, 81, 117, 0.4) 100%);
|
||||
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.Error
|
||||
{
|
||||
color: #ffdddd !important;
|
||||
}
|
||||
|
||||
#text
|
||||
{
|
||||
color: white;
|
||||
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
font-size: 46px;
|
||||
}
|
||||
|
||||
#details
|
||||
{
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
margin-top: -5px;
|
||||
font-size: 16px;
|
||||
color: #f9f9f9;
|
||||
}
|
||||
|
||||
#error-details
|
||||
{
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.el-loading-spinner
|
||||
{
|
||||
top: unset !important;
|
||||
margin-top: 0 !important;
|
||||
width: unset !important;
|
||||
position: unset !important;
|
||||
}
|
||||
|
||||
.el-loading-spinner .path
|
||||
{
|
||||
stroke: white;
|
||||
}
|
||||
</style>
|
||||
@@ -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>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div class="background">
|
||||
<span :style="{width: (score / 5 * 100).toFixed(2) + '%'}" class="rating"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator'
|
||||
|
||||
@Component
|
||||
export default class StarRating extends Vue
|
||||
{
|
||||
@Prop({required: true}) score: number;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.background
|
||||
{
|
||||
background: url("./star-rating-sprite.png") repeat-x;
|
||||
font-size: 0;
|
||||
height: 21px;
|
||||
line-height: 0;
|
||||
overflow: hidden;
|
||||
width: 110px;
|
||||
margin: 0 auto;
|
||||
|
||||
span.rating
|
||||
{
|
||||
background: url("./star-rating-sprite.png") repeat-x 0 100%;
|
||||
float: left;
|
||||
height: 21px;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,33 +1,50 @@
|
||||
/**
|
||||
* 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';
|
||||
/** Base url for api access */
|
||||
static API_URL: string = 'https://va.hydev.org/api';
|
||||
// static API_URL: string = 'http://localhost:24021/api';
|
||||
|
||||
public static SPLASH: string =
|
||||
/** Current version */
|
||||
static VERSION: string = '0.5.6.1761';
|
||||
|
||||
/** The minimum version that still supports the same cookies */
|
||||
static MIN_SUPPORTED_VERSION: string = '0.4.6.1087';
|
||||
|
||||
static GITHUB: string = 'https://github.com/HyDevelop/VeracrossAnalyzer.Client';
|
||||
|
||||
static SPLASH: string =
|
||||
'. , ,---. | \n' +
|
||||
'| |. , |---|,---.,---.| , .,---,,---.,---.\n' +
|
||||
' \\ / >< | || |,---|| | | .-\' |---\'| \n' +
|
||||
' `\' \' ` ` \'` \'`---^`---\'`---|\'---\'`---\'` \n' +
|
||||
' v0.2.3.315 `---\' ';
|
||||
' `---\' \n' +
|
||||
` Version v${Constants.VERSION} by Hykilpikonna (YGui21)\n` +
|
||||
` Github: ${Constants.GITHUB}`;
|
||||
|
||||
// Graph Theme
|
||||
public static THEME =
|
||||
static THEME =
|
||||
{
|
||||
// Colors
|
||||
colors:
|
||||
[
|
||||
'#18cea5',
|
||||
'#4fa8ed',
|
||||
'#f9627b',
|
||||
'#ffb075',
|
||||
'#005c9c',
|
||||
'#bcabe0',
|
||||
'#d36e75',
|
||||
'#19d4ae',
|
||||
'#5ab1ef',
|
||||
'#fa6e86',
|
||||
'#ffb980',
|
||||
'#0067a6',
|
||||
'#c4b4e4',
|
||||
'#d87a80',
|
||||
'#9cbbff',
|
||||
'#d9d0c7',
|
||||
'#87a997',
|
||||
'#d49ea2',
|
||||
'#5b4947',
|
||||
'#7ba3a8',
|
||||
'#fc97af',
|
||||
'#919e8b',
|
||||
'#d7ab82',
|
||||
@@ -39,5 +56,26 @@ export default class Constants
|
||||
'#724e58',
|
||||
'#4b565b'
|
||||
]
|
||||
};
|
||||
|
||||
// Terms (TODO: Actually get the terms dynamically
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
import {CourseUtils} from '@/logic/utils/course-utils';
|
||||
|
||||
export default class CourseInfo
|
||||
{
|
||||
id_ci: number
|
||||
year: number
|
||||
name: string
|
||||
teacher: string
|
||||
level: string
|
||||
courseIds: number[]
|
||||
|
||||
uniqueName: string
|
||||
courseCount: number
|
||||
gradeLevels: number[]
|
||||
enrollments: number
|
||||
classes: ClassInfo[]
|
||||
levelID: number;
|
||||
levelFull: string
|
||||
|
||||
rating: AnalyzedRating = null as any as AnalyzedRating
|
||||
|
||||
/**
|
||||
* Construct with a json object
|
||||
*
|
||||
* @param json
|
||||
*/
|
||||
constructor(json: any)
|
||||
{
|
||||
this.id_ci = json.id_ci
|
||||
this.year = json.year
|
||||
this.name = json.name.trim().replace('&', '&').replace('"', '"')
|
||||
this.teacher = json.teacher
|
||||
this.level = json.level
|
||||
this.courseIds = json.courseIds.split('|').map((id: string) => +id);
|
||||
|
||||
this.courseCount = this.courseIds.length;
|
||||
this.gradeLevels = [];
|
||||
this.uniqueName = CourseInfo.toUniqueName(this.name);
|
||||
this.enrollments = 0;
|
||||
this.classes = []
|
||||
this.levelID = CourseUtils.getLevelID(this.level);
|
||||
this.levelFull = CourseUtils.getLevelFullName(this.level);
|
||||
}
|
||||
|
||||
static toUniqueName(name: string)
|
||||
{
|
||||
return name
|
||||
.replace(/( Semester| Full Year|)/g, '')
|
||||
.replace(/( Accelerated| Honors| College Prep|)/g, '')
|
||||
.replace(/( A| Acc| CP| H| \(.*\))$/g, '');
|
||||
}
|
||||
}
|
||||
|
||||
export class UniqueCourse
|
||||
{
|
||||
name: string
|
||||
courses: CourseInfo[]
|
||||
enrollments: number
|
||||
|
||||
constructor(name: string, courses: CourseInfo[], enrollments: number)
|
||||
{
|
||||
this.name = name;
|
||||
this.courses = courses;
|
||||
this.enrollments = enrollments;
|
||||
}
|
||||
|
||||
get classes()
|
||||
{
|
||||
return this.courses.flatMap(c => c.classes);
|
||||
}
|
||||
}
|
||||
|
||||
export class ClassInfo
|
||||
{
|
||||
id: number
|
||||
name: string
|
||||
teacher: string
|
||||
level: string
|
||||
|
||||
uniqueName: string
|
||||
|
||||
/**
|
||||
* Construct with a json object
|
||||
*
|
||||
* @param json
|
||||
*/
|
||||
constructor(json: any)
|
||||
{
|
||||
this.id = json.id;
|
||||
this.name = json.name.trim().replace('&', '&').replace('"', '"')
|
||||
this.teacher = json.teacher
|
||||
this.level = json.level;
|
||||
|
||||
this.uniqueName = CourseInfo.toUniqueName(this.name);
|
||||
}
|
||||
}
|
||||
|
||||
export class CourseInfoRating
|
||||
{
|
||||
id_ci: number
|
||||
id_user: number
|
||||
firstName: string
|
||||
lastName: string
|
||||
anonymous: boolean
|
||||
ratings: number[]
|
||||
comment: string
|
||||
|
||||
averageRating: number = 0
|
||||
|
||||
constructor(json: any)
|
||||
{
|
||||
this.id_ci = json.id_ci;
|
||||
this.id_user = json.id_user;
|
||||
this.anonymous = this.id_user == -1;
|
||||
this.ratings = json.ratings;
|
||||
this.comment = json.comment;
|
||||
|
||||
if (json.userFullName != null)
|
||||
{
|
||||
let nameSplit = json.userFullName.split(']=[');
|
||||
this.firstName = nameSplit[0];
|
||||
this.lastName = nameSplit[1];
|
||||
}
|
||||
|
||||
this.ratings.forEach(r => this.averageRating += r);
|
||||
this.averageRating /= 5;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new for posting to the server
|
||||
* @param id_ci
|
||||
*/
|
||||
public static createNew(id_ci: number)
|
||||
{
|
||||
return new CourseInfoRating({id_ci: id_ci, id_user: -2, userFullName: null,
|
||||
anonymous: false, ratings: [0,0,0,0,0], comment: ''})
|
||||
}
|
||||
}
|
||||
|
||||
export class AnalyzedRating
|
||||
{
|
||||
ratingCounts: number[][] // ratingCounts[criteria][stars] = count
|
||||
ratingSums: number[] // ratingSums[criteria] = total stars
|
||||
totalCount: number
|
||||
|
||||
ratingAverages: number[] = [] // ratingAverages[criteria] = average
|
||||
overallRating: number = 0
|
||||
|
||||
constructor(json: any)
|
||||
{
|
||||
this.ratingCounts = json.ratingCounts;
|
||||
this.ratingSums = json.ratingSums;
|
||||
this.totalCount = json.totalCount;
|
||||
|
||||
// No ratings
|
||||
if (this.totalCount == 0)
|
||||
{
|
||||
this.overallRating = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate overall rating
|
||||
this.ratingSums.forEach((criteriaScore, i) => this.overallRating += this.ratingAverages[i] = criteriaScore / this.totalCount);
|
||||
this.overallRating /= this.ratingAverages.length;
|
||||
}
|
||||
}
|
||||
|
||||
export const RATING_CRITERIA: {title: string, desc: string}[] =
|
||||
[
|
||||
{title: 'Enjoyable', desc: 'How enjoyable is the course?'},
|
||||
{title: 'Knowledge', desc: 'How interesting is the content of the course? ' +
|
||||
'Is it something you feel worth learning?'},
|
||||
{title: 'Interactivity', desc: 'How interesting is the teacher? Is the teacher interactive?'},
|
||||
{title: 'Eloquence', desc: `Are the teacher's lectures easy to understand?`},
|
||||
{title: 'Fairness', desc: `How fair is the teacher's grading? Is credit given in proportion to effort?`}
|
||||
];
|
||||
@@ -0,0 +1,436 @@
|
||||
import {FormatUtils} from '@/logic/utils/format-utils';
|
||||
import {CourseUtils} from '@/logic/utils/course-utils';
|
||||
import Navigation from '@/components/navigation/navigation';
|
||||
import {GPAUtils} from '@/logic/utils/gpa-utils';
|
||||
import CacheUtils from '@/logic/utils/cache-utils';
|
||||
import Constants from '@/constants';
|
||||
import {Index} from '@/logic/nav-controller';
|
||||
import App from '@/components/app/app';
|
||||
import {CourseInfoRating} from '@/logic/course-info';
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
// Callbacks when this object updates
|
||||
private updateCallbacks: (() => void)[] = [];
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
// 0, 1, 2, 3 contains quarter assignments, 4 contains final assignments
|
||||
if (json.grading_period.toLowerCase() == 'all') this.gradingPeriod = 4;
|
||||
else this.gradingPeriod = +json.grading_period.replace('Quarter ', '') - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Graded or not
|
||||
*/
|
||||
get graded()
|
||||
{
|
||||
// TODO: Add more cases
|
||||
// Incomplete doesn't mean that the teacher didn't grade it yet, which is "Pending".
|
||||
// NREQ is not graded.
|
||||
return this.include && (this.complete == 'Complete' || this.complete == 'Late' || this.complete == 'Incomplete' || this.complete == 'Not Turned In');
|
||||
}
|
||||
|
||||
/**
|
||||
* What is the problem with this assignment
|
||||
*
|
||||
* @return string Empty string if complete, otherwise return problem.
|
||||
*/
|
||||
get problem()
|
||||
{
|
||||
switch (this.complete)
|
||||
{
|
||||
case 'Pending': return 'Pending'; // ID: 0
|
||||
case 'Not Turned In': return 'Not Turned In'; // ID: 1
|
||||
case 'Incomplete': return 'Incomplete'; // ID: 2
|
||||
case 'Complete': return ''; // ID: 3
|
||||
case 'NREQ': return 'Dropped'; // ID: 4
|
||||
case 'Late': return 'Late';
|
||||
default: return this.complete;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the text color of the problem
|
||||
*/
|
||||
get problemColor()
|
||||
{
|
||||
switch (this.complete)
|
||||
{
|
||||
case 'Pending': return '#b1b1b1';
|
||||
case 'Not Turned In': return '#ff0036';
|
||||
case 'Incomplete': return '#ff7a2f';
|
||||
case 'NREQ': return '#41b141';
|
||||
case 'Late': return '#ff7a2f';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add callback
|
||||
*
|
||||
* @param callback
|
||||
*/
|
||||
addCallback(callback: () => void)
|
||||
{
|
||||
this.updateCallbacks.push(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark as read
|
||||
*/
|
||||
markAsRead(): Promise<void>
|
||||
{
|
||||
return new Promise((resolve, reject) => {
|
||||
App.http.post('/mark-as-read', {scoreId: this.scoreId})
|
||||
.then(response =>
|
||||
{
|
||||
// Check success
|
||||
if (response.success)
|
||||
{
|
||||
this.unread = false;
|
||||
this.updateCallbacks.forEach(callback => callback());
|
||||
resolve();
|
||||
}
|
||||
else reject(response.data);
|
||||
})
|
||||
.catch(reject)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export interface AssignmentType
|
||||
{
|
||||
id: number
|
||||
name: string
|
||||
|
||||
weight: number
|
||||
scoreMax: number
|
||||
score: number
|
||||
percent: number
|
||||
assignmentCount: number
|
||||
|
||||
graded: boolean
|
||||
}
|
||||
|
||||
export interface Grading
|
||||
{
|
||||
method: string
|
||||
weightingMap: {[index: string]: number}
|
||||
}
|
||||
|
||||
export default class Course
|
||||
{
|
||||
id: number
|
||||
id_ci: number
|
||||
assignmentsId: number
|
||||
name: string
|
||||
teacherName: string
|
||||
status: string
|
||||
rawAssignments: Assignment[]
|
||||
rating: CourseInfoRating
|
||||
rated: boolean
|
||||
|
||||
rawLetterGrade?: string
|
||||
rawNumericGrade?: number
|
||||
|
||||
level: string
|
||||
scaleUp: number
|
||||
|
||||
termGrading: Grading[]
|
||||
termAssignments: Assignment[][]
|
||||
|
||||
cache: CacheUtils = new CacheUtils();
|
||||
|
||||
/**
|
||||
* Construct a course with a course json object
|
||||
*
|
||||
* @param courseJson Course json object
|
||||
*/
|
||||
constructor(courseJson: any)
|
||||
{
|
||||
this.id = courseJson.id;
|
||||
this.id_ci = courseJson.id_ci;
|
||||
this.assignmentsId = courseJson.assignmentsId;
|
||||
this.name = FormatUtils.parseText(courseJson.name).trim();
|
||||
this.teacherName = courseJson.teacherName;
|
||||
this.status = courseJson.status;
|
||||
this.rated = courseJson.rating != null;
|
||||
this.rating = this.rated ? new CourseInfoRating(courseJson.rating) : CourseInfoRating.createNew(this.id_ci);
|
||||
|
||||
this.rawLetterGrade = courseJson.letterGrade;
|
||||
this.rawNumericGrade = courseJson.numericGrade;
|
||||
|
||||
// Other api issue
|
||||
if (this.rawLetterGrade == '')
|
||||
{
|
||||
this.rawNumericGrade = undefined;
|
||||
this.rawLetterGrade = undefined;
|
||||
}
|
||||
|
||||
// Level and scaleUp TODO: Use server course level
|
||||
let level = CourseUtils.getLevel(courseJson.level);
|
||||
this.level = level.level;
|
||||
this.scaleUp = level.scaleUp;
|
||||
|
||||
this.termGrading = new Array(4).fill(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load in assignments data
|
||||
*
|
||||
* @param data Assignments data
|
||||
*/
|
||||
loadAssignments(data: any)
|
||||
{
|
||||
// Load assignments
|
||||
// Parse json and filter it
|
||||
this.rawAssignments = data.assignments.map((a: any) => new Assignment(a));
|
||||
|
||||
// Sort by date (Latest is at 0)
|
||||
this.rawAssignments.sort((a, b) => b.time - a.time);
|
||||
|
||||
// Filter assignments into terms
|
||||
this.termAssignments = [[], [], [], [], []];
|
||||
|
||||
// Loop through it by time order
|
||||
this.rawAssignments.forEach(a => this.termAssignments[a.gradingPeriod].push(a));
|
||||
}
|
||||
|
||||
/**
|
||||
* Is graded or not
|
||||
*/
|
||||
get isGraded(): boolean
|
||||
{
|
||||
// Skip future or past courses
|
||||
if (this.status != 'active') return false;
|
||||
|
||||
// Skip courses without levels TODO: Ask for user input
|
||||
if (this.level == 'None' || this.level == 'Unknown' || this.scaleUp == -1) return false;
|
||||
|
||||
// Skip courses without graded assignments
|
||||
if (this.assignments.length == 0) return false;
|
||||
|
||||
// Skip if there are no grading scale
|
||||
// if (course.grading.method == 'NOT_GRADED') return;
|
||||
|
||||
// Is graded
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get currently selected grading periods
|
||||
*/
|
||||
get gradingPeriods(): number[]
|
||||
{
|
||||
return this.cache.get('GradingPeriods', () =>
|
||||
{
|
||||
return (this.rawSelectedTerm == -1 ? [0, 1, 2, 3] : [this.rawSelectedTerm]).filter(term =>
|
||||
this.termAssignments[term].filter(a => a.graded).length != 0);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get currently selected grading periods
|
||||
*/
|
||||
get allGradingPeriods(): number[]
|
||||
{
|
||||
return this.cache.get('AllGradingPeriods', () =>
|
||||
{
|
||||
return [0, 1, 2, 3].filter(term => this.termAssignments[term].filter(a => a.graded).length != 0);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get assignments of the selected grading periods
|
||||
*/
|
||||
get assignments(): Assignment[]
|
||||
{
|
||||
return this.gradingPeriods
|
||||
.flatMap(term => this.termAssignments[term])
|
||||
.sort((a, b) => b.time - a.time);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
return {term: term, assignments: assignments}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get letter grade
|
||||
*/
|
||||
get letterGrade(): string
|
||||
{
|
||||
return this.cache.get('LetterGrade', () =>
|
||||
{
|
||||
// Get scale
|
||||
let scale = GPAUtils.findScale(this.numericGrade);
|
||||
|
||||
// Scale not found
|
||||
return scale == undefined ? '--' : scale.letter;
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get letter grade by term
|
||||
*
|
||||
* @param term
|
||||
*/
|
||||
letterGradeTerm(term: number): string
|
||||
{
|
||||
return this.cache.get('LetterGrade' + term, () =>
|
||||
{
|
||||
// Get scale
|
||||
let scale = GPAUtils.findScale(this.numericGradeTerm(term));
|
||||
|
||||
// Scale not found
|
||||
return scale == undefined ? '--' : scale.letter;
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get numeric grade
|
||||
*/
|
||||
get numericGrade()
|
||||
{
|
||||
return this.cache.get('NumericGrade', () =>
|
||||
{
|
||||
return this.gradingPeriods.map(term => this.numericGradeTerm(term))
|
||||
.reduce((p, v) => p + v) / this.gradingPeriods.length
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get numeric grade by term
|
||||
*
|
||||
* @param term
|
||||
*/
|
||||
numericGradeTerm(term: number): number
|
||||
{
|
||||
return this.cache.get('NumericGrade' + term, () =>
|
||||
{
|
||||
// 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;
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
// Get graded assignments
|
||||
let gradedAssignments = typeAssignments.filter(a => a.graded);
|
||||
|
||||
// Count scores and max scores
|
||||
let score = gradedAssignments.reduce((sum, a) => sum + a.score, 0);
|
||||
let scoreMax = gradedAssignments.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, graded: gradedAssignments.length > 0}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get url hash code
|
||||
*/
|
||||
get urlHash(): string
|
||||
{
|
||||
return `course/${this.id}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Get navigation index
|
||||
*/
|
||||
get urlIndex(): Index
|
||||
{
|
||||
return {hash: this.urlHash, title: this.name, identifier: 'course', info: {id: this.id}}
|
||||
}
|
||||
|
||||
/**
|
||||
* Selected term
|
||||
*/
|
||||
get rawSelectedTerm(): number
|
||||
{
|
||||
return Navigation.instance.getSelectedTerm()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import {GPAUtils} from '@/logic/utils/gpa-utils';
|
||||
import Course from '@/logic/course';
|
||||
|
||||
const md5 = require('md5');
|
||||
|
||||
export default class LoginUser
|
||||
{
|
||||
id: number
|
||||
schoolPersonPk: number
|
||||
username: string
|
||||
lastLogin: Date
|
||||
firstLogin: Date
|
||||
firstName: string
|
||||
lastName: string
|
||||
graduationYear: number
|
||||
emails: string[]
|
||||
classes: string[]
|
||||
avatarUrl: string
|
||||
|
||||
token: string
|
||||
courses: Course[]
|
||||
|
||||
gradeLevel: number
|
||||
gradeLevelName: string
|
||||
|
||||
constructor(jsonData: any)
|
||||
{
|
||||
let json = jsonData.user
|
||||
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.graduationYear = +json.graduationYear;
|
||||
this.emails = json.emails.split('|').map((e: any) => e.toLowerCase().trim());
|
||||
this.classes = json.classes.split('|');
|
||||
this.avatarUrl = json.avatarUrl;
|
||||
|
||||
// Extracted in newer versions
|
||||
this.token = jsonData.token;
|
||||
this.courses = jsonData.courses.map((courseJson: any) => new Course(courseJson));
|
||||
|
||||
// Calculated grade level
|
||||
this.gradeLevel = GPAUtils.getGradeLevel(this.graduationYear);
|
||||
this.gradeLevelName = GPAUtils.gradeLevelName(this.gradeLevel);
|
||||
|
||||
// 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,160 @@
|
||||
import {FormatUtils} from '@/logic/utils/format-utils';
|
||||
import pWaitFor from 'p-wait-for';
|
||||
import App from '@/components/app/app';
|
||||
|
||||
export interface Index
|
||||
{
|
||||
hash: string
|
||||
title?: string
|
||||
identifier: string
|
||||
info?: any
|
||||
}
|
||||
|
||||
export default class NavController
|
||||
{
|
||||
// Current index
|
||||
index: Index;
|
||||
|
||||
// Callback
|
||||
updateCallback?: () => void;
|
||||
|
||||
constructor()
|
||||
{
|
||||
// Create history state listener
|
||||
window.onpopstate = (e: any) =>
|
||||
{
|
||||
if (e.state)
|
||||
{
|
||||
// Restore previous tab
|
||||
//console.log(`onPopState: Current: ${this.index.hash}, Previous: ${e.state.hash}`);
|
||||
this.updateIndex(e.state, false);
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize
|
||||
this.init()
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize from last location
|
||||
*/
|
||||
private init()
|
||||
{
|
||||
if (window.location.hash == '#info') return;
|
||||
|
||||
// Check history from last session
|
||||
if (window.history.state != undefined && window.history.state.hash != undefined)
|
||||
{
|
||||
// Last history exists
|
||||
this.index = window.history.state;
|
||||
return;
|
||||
}
|
||||
|
||||
// Last history doesn't exist but hash url might exist
|
||||
let hash = window.location.hash.replace('#', '');
|
||||
|
||||
// Check hash
|
||||
if (hash == '')
|
||||
{
|
||||
// No location info in url, set page to overall
|
||||
window.history.replaceState(this.convertIndex('overall'), '', '/#overall');
|
||||
this.updateIndex('overall', false);
|
||||
return;
|
||||
}
|
||||
|
||||
// There is hash info in url
|
||||
let split = hash.split('/');
|
||||
|
||||
// Not course -> don't know what to do with this url, so just refresh
|
||||
if (split[0] != 'course')
|
||||
{
|
||||
this.initClear();
|
||||
return;
|
||||
}
|
||||
|
||||
// Is course -> Update index with placeholder title
|
||||
this.updateIndex({hash: hash, title: `Loading...`, identifier: 'course', info: {id: +split[1]}}, false);
|
||||
|
||||
// Wait for courses to finish loading
|
||||
pWaitFor(() => App.instance != undefined && App.instance.assignmentsReady).then(() =>
|
||||
{
|
||||
// Find course
|
||||
let course = App.instance.courses.find(c => c.id == +split[1]);
|
||||
|
||||
// This person has no such course, refresh to overall
|
||||
if (course == null)
|
||||
{
|
||||
this.initClear();
|
||||
return;
|
||||
}
|
||||
|
||||
window.history.replaceState(course.urlIndex, '', '/#' + course.urlHash);
|
||||
this.updateIndex(course.urlIndex, false);
|
||||
})
|
||||
}
|
||||
|
||||
private initClear()
|
||||
{
|
||||
window.location.hash = '';
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update index
|
||||
*
|
||||
* @param index Hash and title | Hash only
|
||||
* @param history Record in history or not (Default true)
|
||||
*/
|
||||
updateIndex(index: Index | string, history: boolean = true)
|
||||
{
|
||||
index = this.convertIndex(index);
|
||||
|
||||
// Call custom event
|
||||
if (this.updateCallback != null) this.updateCallback();
|
||||
|
||||
// Record history or not
|
||||
if (history)
|
||||
{
|
||||
//console.log(`history: Current: ${this.index.hash}, New: ${index.hash}`);
|
||||
|
||||
// Check url
|
||||
let url = `/#${index.hash}`;
|
||||
|
||||
// Push history state
|
||||
window.history.pushState(index, '', url);
|
||||
}
|
||||
|
||||
// Update title
|
||||
document.title = 'Veracross Analyzer - ' + index.title;
|
||||
|
||||
// Scroll to top
|
||||
window.scrollTo(0, 0);
|
||||
|
||||
// Update selected index
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check index conversion
|
||||
*
|
||||
* @param index Hash and title | Hash only
|
||||
* @return Index Hash and title
|
||||
*/
|
||||
private convertIndex(index: Index | string): Index
|
||||
{
|
||||
// Convert index format if it is hash only
|
||||
if (typeof index == 'string') index = {hash: index, identifier: index};
|
||||
if (index.title == null) index.title = FormatUtils.toTitleCase(index.hash);
|
||||
return index;
|
||||
}
|
||||
|
||||
get id(): string
|
||||
{
|
||||
return this.index.identifier
|
||||
}
|
||||
|
||||
get info(): any
|
||||
{
|
||||
return this.index.info
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import Navigation from '@/components/navigation/navigation';
|
||||
import Constants from '@/constants';
|
||||
import {isNumeric} from '@/logic/utils/general-utils';
|
||||
|
||||
const LEVEL_AP = {level: 'AP', scaleUp: 1};
|
||||
const LEVEL_H = {level: 'H', scaleUp: 0.75};
|
||||
const LEVEL_A = {level: 'A', scaleUp: 0.5};
|
||||
const LEVEL_CP = {level: 'CP', scaleUp: 0.25};
|
||||
const LEVEL_CLUB = {level: 'Club', scaleUp: -1};
|
||||
const LEVEL_SPORT = {level: 'Sport', scaleUp: -1};
|
||||
const LEVEL_NONE = {level: 'None', scaleUp: -1};
|
||||
const LEVEL_UNKNOWN = {level: 'Unknown', scaleUp: -1};
|
||||
|
||||
export class CourseUtils
|
||||
{
|
||||
/**
|
||||
* 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];
|
||||
}
|
||||
|
||||
static getLevelID(level: string)
|
||||
{
|
||||
if (level == undefined) return -1;
|
||||
|
||||
level = level.toLowerCase();
|
||||
|
||||
if (level == 'ap' || level == 'advanced placement') return 1;
|
||||
if (level == 'h' || level == 'honors') return 2;
|
||||
if (level == 'a' || level == 'acc' || level == 'accelerated') return 3;
|
||||
if (level == 'cp' || level == 'college prep') return 4;
|
||||
|
||||
if (level == 'club') return 101;
|
||||
if (level == 'sport') return 102;
|
||||
|
||||
if (level == 'none') return 201;
|
||||
|
||||
if (isNumeric(level)) return +level;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full name of a level from short name
|
||||
*
|
||||
* @param level Any level name
|
||||
*/
|
||||
static getLevelFullName(level: string)
|
||||
{
|
||||
switch (this.getLevelID(level))
|
||||
{
|
||||
case 1: return 'AP';
|
||||
case 2: return 'Honors';
|
||||
case 3: return 'Accelerated';
|
||||
case 4: return 'CP';
|
||||
case 101: return 'Club';
|
||||
case 102: return 'Sport';
|
||||
case 201: return 'None';
|
||||
default: return '--';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full name of a level from short name
|
||||
*
|
||||
* @param level Any level name
|
||||
*/
|
||||
static getLevel(level: string)
|
||||
{
|
||||
switch (this.getLevelID(level))
|
||||
{
|
||||
case 1: return LEVEL_AP;
|
||||
case 2: return LEVEL_H;
|
||||
case 3: return LEVEL_A;
|
||||
case 4: return LEVEL_CP;
|
||||
case 101: return LEVEL_CLUB;
|
||||
case 102: return LEVEL_SPORT;
|
||||
case 201: return LEVEL_NONE;
|
||||
default: return LEVEL_UNKNOWN;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
export class FormatUtils
|
||||
{
|
||||
/**
|
||||
* Limit string length
|
||||
*
|
||||
* @param str String
|
||||
* @param length Max length
|
||||
*/
|
||||
public static limit(str: string, length: number): string
|
||||
{
|
||||
return str.length <= length ? str : str.substr(0, length - 2) + '...'
|
||||
}
|
||||
|
||||
/**
|
||||
* To Title Case
|
||||
*
|
||||
* @param str oRigInAL sTrING
|
||||
* @return string Original String
|
||||
*/
|
||||
public static toTitleCase(str: string)
|
||||
{
|
||||
return str.replace(/\w\S*/g, s => s.charAt(0).toUpperCase() + s.substr(1).toLowerCase())
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse html text
|
||||
*
|
||||
* @param str
|
||||
*/
|
||||
public static parseText(str: string): string
|
||||
{
|
||||
return str.replace(/&/g, '&');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export function isNumeric(str: string)
|
||||
{
|
||||
return !isNaN(parseFloat(str)) && isFinite(+str);
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
import Course, {Assignment, Grading} from '@/logic/course';
|
||||
|
||||
export interface Scale
|
||||
{
|
||||
min: number
|
||||
letter: string
|
||||
gp: number
|
||||
}
|
||||
|
||||
/**
|
||||
* This is an utility class to calculate GPA.
|
||||
*/
|
||||
export class GPAUtils
|
||||
{
|
||||
// [[Min score, Letter grade, Base GPA], ...]
|
||||
public static SCALE: Scale[] =
|
||||
[
|
||||
{min: 96.5, letter: 'A+', gp: 4.00},
|
||||
{min: 92.5, letter: 'A' , gp: 3.75},
|
||||
{min: 89.5, letter: 'A-', gp: 3.50},
|
||||
{min: 86.5, letter: 'B+', gp: 3.25},
|
||||
{min: 82.5, letter: 'B' , gp: 3.00},
|
||||
{min: 79.5, letter: 'B-', gp: 2.75},
|
||||
{min: 76.5, letter: 'C+', gp: 2.50},
|
||||
{min: 72.5, letter: 'C' , gp: 2.25},
|
||||
{min: 70.5, letter: 'C-', gp: 2.00},
|
||||
{min: 69.5, letter: 'D' , gp: 1.00},
|
||||
{min: 0 , letter: 'F' , gp: 0.00}
|
||||
];
|
||||
|
||||
/**
|
||||
* Calculate GPA for a list of couses
|
||||
*
|
||||
* @param coursesOriginal List of courses
|
||||
*/
|
||||
public static getGPA(coursesOriginal: Course[]): {gpa: number, accurate: boolean, max: number}
|
||||
{
|
||||
// Clone array
|
||||
let courses: Course[] = [];
|
||||
|
||||
// Accurate or not
|
||||
let accurate: boolean = true;
|
||||
|
||||
// Remove all courses that does not have a grade
|
||||
coursesOriginal.forEach(course =>
|
||||
{
|
||||
if (course.letterGrade == null || course.letterGrade == '')
|
||||
{
|
||||
accurate = false;
|
||||
}
|
||||
else if (course.level != 'none' && !isNaN(course.numericGrade))
|
||||
{
|
||||
courses.push(course);
|
||||
}
|
||||
});
|
||||
|
||||
// If no course have grade, return -1
|
||||
if (courses.length == 0)
|
||||
{
|
||||
return {gpa: -1, accurate: false, max: -1};
|
||||
}
|
||||
|
||||
// Count total GPA
|
||||
let totalGPA = 0;
|
||||
let maxTotal = 0;
|
||||
courses.forEach(course =>
|
||||
{
|
||||
totalGPA += this.getGP(course, course.numericGrade);
|
||||
maxTotal += this.getGP(course, 'A+');
|
||||
});
|
||||
|
||||
// Get average GPA, round to two decimal places
|
||||
let gpa = Math.round(totalGPA / courses.length * 100) / 100;
|
||||
let maxGPA = Math.round(maxTotal / courses.length * 100) / 100;
|
||||
|
||||
// Return results
|
||||
return {gpa: gpa, accurate: accurate, max: maxGPA};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate GPA for a course
|
||||
*
|
||||
* @param course Course
|
||||
* @param letterGrade Letter grade
|
||||
*/
|
||||
public static getGP(course: Course, letterGrade: string | number): number
|
||||
{
|
||||
// Get scale
|
||||
let scale = this.findScale(letterGrade);
|
||||
|
||||
// No scale
|
||||
if (scale == undefined) return -1;
|
||||
|
||||
// Add scaleUp if not failed.
|
||||
return scale.gp == 0 ? 0 : scale.gp + course.scaleUp;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the scale for a grade
|
||||
*
|
||||
* @param grade Letter grade or numeric grade
|
||||
*/
|
||||
public static findScale(grade: string | number): Scale | undefined
|
||||
{
|
||||
// Letter grade
|
||||
if (typeof grade == 'string')
|
||||
{
|
||||
return this.SCALE.find(scale => scale.letter == grade);
|
||||
}
|
||||
|
||||
// Numeric grade
|
||||
return this.SCALE.find(scale => grade >= scale.min);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the total-mean (total/max) average
|
||||
*
|
||||
* @param assignments
|
||||
*/
|
||||
public static getTotalMeanAverage(assignments: Assignment[])
|
||||
{
|
||||
let score = 0;
|
||||
let max = 0;
|
||||
|
||||
// Loop through assignments
|
||||
assignments.forEach(assignment =>
|
||||
{
|
||||
// If assignment should be displayed
|
||||
if (!assignment.graded) return;
|
||||
|
||||
// Record scores
|
||||
score += assignment.score;
|
||||
max += assignment.scoreMax;
|
||||
});
|
||||
|
||||
// Return
|
||||
return +(score / max * 100).toFixed(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the percent type
|
||||
*
|
||||
* @param grading
|
||||
* @param assignments
|
||||
*/
|
||||
public static getPercentTypeAverage(grading: Grading, assignments: Assignment[])
|
||||
{
|
||||
let typeScores: {[index: string]: any} = {};
|
||||
let typeCounts: {[index: string]: any} = {};
|
||||
|
||||
// Loop through assignments
|
||||
assignments.forEach(assignment =>
|
||||
{
|
||||
// If assignment should be displayed
|
||||
if (!assignment.graded) return;
|
||||
|
||||
// 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 grading.weightingMap)
|
||||
{
|
||||
if (typeScores[type] != undefined)
|
||||
{
|
||||
totalPercentage += grading.weightingMap[type];
|
||||
}
|
||||
}
|
||||
|
||||
// Count
|
||||
let score = 0;
|
||||
for (let type in typeScores)
|
||||
{
|
||||
let typeFactor = grading.weightingMap[type] / totalPercentage;
|
||||
score += typeScores[type] * typeFactor / typeCounts[type];
|
||||
}
|
||||
|
||||
// Add average to the row
|
||||
return +(score * 100).toFixed(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current school year
|
||||
*/
|
||||
public static getSchoolYear(): number
|
||||
{
|
||||
// Get current year
|
||||
let currentYear = new Date().getFullYear();
|
||||
|
||||
// Convert current year to current school year: +1 if it's after August
|
||||
if (new Date().getMonth() > 7) currentYear ++;
|
||||
|
||||
return currentYear;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get grade level from graduation year
|
||||
*
|
||||
* @param graduationYear
|
||||
*/
|
||||
public static getGradeLevel(graduationYear: number): number
|
||||
{
|
||||
// Calculate grade level
|
||||
return 12 - (graduationYear - this.getSchoolYear());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get grade level name from grade level. (Eg. Freshman, Sophomore, etc.)
|
||||
*
|
||||
* @param gradeLevel
|
||||
*/
|
||||
public static gradeLevelName(gradeLevel: number): string
|
||||
{
|
||||
switch (gradeLevel)
|
||||
{
|
||||
case 9: return 'Freshman';
|
||||
case 10: return 'Sophomore';
|
||||
case 11: return 'Junior';
|
||||
case 12: return 'Senior';
|
||||
|
||||
default: return 'Grade ' + gradeLevel;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import Constants from '@/constants';
|
||||
import App from '@/components/app/app';
|
||||
|
||||
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)
|
||||
{
|
||||
// TODO: Auto update after switching dark mode (possibly by refreshing)
|
||||
opacity = App.instance.darkMode ? 0.1 : opacity;
|
||||
|
||||
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,18 +1,14 @@
|
||||
import Constants from '@/constants';
|
||||
import LoginUser from '@/logic/login-user';
|
||||
|
||||
export class HttpUtils
|
||||
{
|
||||
public token: string = '';
|
||||
|
||||
constructor (token: string)
|
||||
{
|
||||
this.token = token;
|
||||
}
|
||||
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) =>
|
||||
@@ -32,4 +28,25 @@ export class HttpUtils
|
||||
.catch(reject)
|
||||
});
|
||||
}
|
||||
|
||||
public get(url: string): Promise<any>
|
||||
{
|
||||
// Create promise
|
||||
return new Promise<any>((resolve, reject) =>
|
||||
{
|
||||
// Fetch request
|
||||
fetch(url, {method: 'GET'}).then(res =>
|
||||
{
|
||||
// Get response body text
|
||||
res.text().then(text =>
|
||||
{
|
||||
// Parse response
|
||||
let response = JSON.parse(text);
|
||||
resolve(response);
|
||||
})
|
||||
.catch(reject)
|
||||
})
|
||||
.catch(reject)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
export default class VersionUtils
|
||||
{
|
||||
/**
|
||||
* Compare two version numbers
|
||||
*
|
||||
* Eg.
|
||||
* compare('0.1.2', '0.1.3') = -1
|
||||
* compare('1.0.0', '0.1.3') = 1
|
||||
* compare('0.0.1', '0.0.1') = 0
|
||||
*
|
||||
* @param ver1 Version 1
|
||||
* @param ver2 Version 2
|
||||
* @return number (-1 if ver1 < ver2), (1 if ver1 > ver2), (0 if equal)
|
||||
*/
|
||||
public static compare(ver1: string, ver2: string): number
|
||||
{
|
||||
// Equal case
|
||||
if (ver1 == ver2) return 0;
|
||||
|
||||
// Split
|
||||
let split1 = ver1.split('.');
|
||||
let split2 = ver2.split('.');
|
||||
|
||||
// Detect each number
|
||||
for (let i in split1)
|
||||
{
|
||||
// Get numbers
|
||||
let num1 = split1[i];
|
||||
let num2 = split2[i];
|
||||
|
||||
// Current number is equal
|
||||
if (num1 == num2) continue;
|
||||
|
||||
// Current number is different
|
||||
return +num1 < +num2 ? -1 : 1;
|
||||
}
|
||||
|
||||
// Equal
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import Vue from 'vue';
|
||||
import ElementUI from 'element-ui';
|
||||
const VCharts = require('v-charts');
|
||||
|
||||
import App from './components/app/app.vue';
|
||||
import VueCookies from 'vue-cookies';
|
||||
|
||||
const VCharts = require('v-charts');
|
||||
|
||||
Vue.config.productionTip = false;
|
||||
|
||||
// Use Element UI
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
|
||||
#course-list
|
||||
{
|
||||
margin-right: 20px;
|
||||
height: 70vh;
|
||||
|
||||
.padding-fix
|
||||
{
|
||||
// Fix width issue
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.header
|
||||
{
|
||||
.text
|
||||
{
|
||||
margin-top: 15px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.search
|
||||
{
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.list
|
||||
{
|
||||
overflow: scroll;
|
||||
overflow-x: hidden;
|
||||
height: 377px;
|
||||
}
|
||||
|
||||
// Remove scrollbar
|
||||
.list::-webkit-scrollbar
|
||||
{
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.item
|
||||
{
|
||||
text-align: left;
|
||||
margin-bottom: 15px;
|
||||
|
||||
background: #fbfbfb;
|
||||
border-radius: 4px;
|
||||
padding: 5px 10px;
|
||||
|
||||
.name
|
||||
{
|
||||
// Text too long
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.data
|
||||
{
|
||||
// text-align: right;
|
||||
color: #a5a5a5;
|
||||
|
||||
span
|
||||
{
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
// text-align: left;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cards
|
||||
.left
|
||||
{
|
||||
margin-left: 20px;
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator'
|
||||
import App from '@/components/app/app';
|
||||
import CourseInfo, {ClassInfo, UniqueCourse} from '@/logic/course-info';
|
||||
import {GPAUtils} from '@/logic/utils/gpa-utils';
|
||||
// @ts-ignore
|
||||
import SearchSettingsComponent, {SearchSettings} from '@/pages/course-selection/pages/search-settings.vue';
|
||||
import Welcome from '@/pages/course-selection/pages/welcome.vue';
|
||||
import CourseDetail from '@/pages/course-selection/pages/course-detail.vue';
|
||||
import LoadingSpinner from '@/components/loading-spinner.vue';
|
||||
|
||||
@Component({components: {SearchSettings: SearchSettingsComponent, Welcome, CourseDetail, LoadingSpinner}})
|
||||
export default class CourseSelection extends Vue
|
||||
{
|
||||
@Prop({required: true}) app: App
|
||||
|
||||
search: string = ''
|
||||
courseInfo: CourseInfo[] = []
|
||||
courseIdIndex: any = {} // Map<CourseID, index in courseInfo>
|
||||
directory: {gradeLevel: number, classes: string}[] = []
|
||||
classes: ClassInfo[] = []
|
||||
loading = true
|
||||
|
||||
courseListHeight: number = 0;
|
||||
cardsHeight: number = 0;
|
||||
|
||||
openedPage: string = '';
|
||||
settings: SearchSettings = new SearchSettings();
|
||||
activeCourse: UniqueCourse = new UniqueCourse('', [], -1);
|
||||
|
||||
/**
|
||||
* Called before rendering
|
||||
*/
|
||||
created()
|
||||
{
|
||||
// Update width dynamically
|
||||
window.addEventListener('resize', this.updateHeight);
|
||||
|
||||
// Get courses
|
||||
App.http.post('/course-info', {}).then(result =>
|
||||
{
|
||||
if (result.success)
|
||||
{
|
||||
// Parse data
|
||||
this.classes = result.data.classes.map((json: any) => new ClassInfo(json));
|
||||
this.courseInfo = result.data.courseInfos.map((json: any, index: number) =>
|
||||
{
|
||||
let info = new CourseInfo(json);
|
||||
|
||||
// Index
|
||||
info.courseIds.forEach(id =>
|
||||
{
|
||||
this.courseIdIndex[id] = index;
|
||||
|
||||
// Add class info into course
|
||||
let classInfo = this.classes.find(c => c.id == id)
|
||||
if (classInfo == null) return;
|
||||
info.classes.push(classInfo);
|
||||
});
|
||||
return info;
|
||||
});
|
||||
this.directory = result.data.studentInfos;
|
||||
|
||||
// Use directory data
|
||||
this.directory.forEach(d =>
|
||||
{
|
||||
d.classes.split('|').forEach(classId =>
|
||||
{
|
||||
// Get info by class id
|
||||
let info = this.courseInfo[this.courseIdIndex[+classId]];
|
||||
if (info as any != null)
|
||||
{
|
||||
// Add grade level
|
||||
if (!info.gradeLevels.includes(d.gradeLevel))
|
||||
{
|
||||
info.gradeLevels.push(d.gradeLevel);
|
||||
}
|
||||
|
||||
// Count enrollments
|
||||
info.enrollments ++;
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
this.loading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Called on destroy
|
||||
*/
|
||||
destroyed()
|
||||
{
|
||||
// Remove width updater
|
||||
window.removeEventListener('resize', this.updateHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called on vue update
|
||||
*/
|
||||
updated()
|
||||
{
|
||||
this.updateHeight()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update header height. (CSS doesn't work)
|
||||
*/
|
||||
updateHeight()
|
||||
{
|
||||
// Get element
|
||||
let cl = this.$refs.cl as Vue;
|
||||
if (cl as any == null) return;
|
||||
let el = cl.$el;
|
||||
|
||||
// Calculate height
|
||||
this.cardsHeight = window.innerHeight - el.getBoundingClientRect().top - 20;
|
||||
this.courseListHeight = this.cardsHeight - 15 - 102;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the settings page.
|
||||
*/
|
||||
openSettings()
|
||||
{
|
||||
this.openedPage = this.openedPage == 'settings' ? '' : 'settings';
|
||||
}
|
||||
|
||||
/**
|
||||
* Open course page.
|
||||
*/
|
||||
openCourse(course: UniqueCourse)
|
||||
{
|
||||
if (this.activeCourse == course)
|
||||
{
|
||||
this.openedPage = '';
|
||||
this.activeCourse = null as any as UniqueCourse;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.activeCourse = course;
|
||||
this.openedPage = 'course'
|
||||
}
|
||||
}
|
||||
|
||||
get filteredCourses()
|
||||
{
|
||||
let year = GPAUtils.getSchoolYear();
|
||||
|
||||
return this.courseInfo.filter(c =>
|
||||
c.uniqueName.toLowerCase().includes(this.search.toLowerCase()) &&
|
||||
c.level != null && this.settings.levels.includes(c.level) &&
|
||||
c.year == year &&
|
||||
(this.settings.showAllCourses || c.gradeLevels.includes(this.app.user.gradeLevel + 1))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets unique courses by name, even though many different teachers might teach it.
|
||||
*/
|
||||
get uniqueCourses(): UniqueCourse[]
|
||||
{
|
||||
let names: string[] = [];
|
||||
let list: UniqueCourse[] = [];
|
||||
|
||||
this.filteredCourses.forEach(c =>
|
||||
{
|
||||
// Create the course list if doesn't exist
|
||||
if (!names.includes(c.uniqueName))
|
||||
{
|
||||
names.push(c.uniqueName);
|
||||
list.push(new UniqueCourse(c.uniqueName, [], 0))
|
||||
}
|
||||
|
||||
// Add the course
|
||||
list[names.indexOf(c.uniqueName)].courses.push(c);
|
||||
list[names.indexOf(c.uniqueName)].enrollments += c.enrollments;
|
||||
})
|
||||
|
||||
// Sorting
|
||||
switch (this.settings.sortBy)
|
||||
{
|
||||
case 'Popularity':
|
||||
{
|
||||
list.sort((a, b) => b.enrollments - a.enrollments);
|
||||
break
|
||||
}
|
||||
default:
|
||||
{
|
||||
list.sort((a, b) => a.name.localeCompare(b.name));
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div id="course-selection">
|
||||
<div v-if="loading" class="loading vertical-center" style="height: 100%">
|
||||
<LoadingSpinner style="left: 0"/>
|
||||
</div>
|
||||
|
||||
<el-row v-else>
|
||||
<el-col :span="16" class="overall-span">
|
||||
<el-card class="left" :style="{height: cardsHeight + 'px'}">
|
||||
<SearchSettings v-if="openedPage === 'settings'" ref="settings" :settings="settings"/>
|
||||
<Welcome v-if="openedPage === ''" :app="app"/>
|
||||
<CourseDetail v-if="openedPage === 'course'" :unique-course="activeCourse"/>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- Course list card -->
|
||||
<el-col :span="8" class="overall-span">
|
||||
<el-card id="course-list" class="right" ref="cl" body-style="padding: 0" :style="{height: cardsHeight + 'px'}">
|
||||
<div class="header padding-fix">
|
||||
<div class="text">Course List</div>
|
||||
|
||||
<!-- Search -->
|
||||
<el-input class="search" placeholder="Search..." prefix-icon="el-icon-search" v-model="search">
|
||||
<el-button slot="append" icon="el-icon-s-tools" @click="openSettings"/>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<!-- Actual course list -->
|
||||
<div class="list padding-fix" :style="{height: courseListHeight + 'px'}">
|
||||
<!-- Every course -->
|
||||
<div v-for="(course, index) in uniqueCourses" class="item vertical-center clickable unselectable"
|
||||
@click="openCourse(course)">
|
||||
|
||||
<div class="name">{{course.name}}</div>
|
||||
<div class="data">
|
||||
<span class="classes"><i class="el-icon-s-home"/> {{course.classes.length}}</span>
|
||||
<span class="teachers"><i class="el-icon-user-solid"/> {{course.courses.length}}</span>
|
||||
<span class="enrollments"><i class="el-icon-user"/> {{course.enrollments}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./course-selection.ts" lang="ts"/>
|
||||
<style src="./course-selection.scss" lang="scss" scoped/>
|
||||
@@ -0,0 +1,269 @@
|
||||
<template>
|
||||
<div id="course-detail">
|
||||
<div class="header">Course: <span style="color: #229fff">{{uniqueCourse.name}}</span></div>
|
||||
<el-divider class="divider"><i class="el-icon-reading"></i></el-divider>
|
||||
|
||||
<!-- All course-infos -->
|
||||
<div class="item clickable unselectable" v-for="c in sortedCourses" @click="openDetails(c)">
|
||||
<div class="float-left">
|
||||
<div>{{c.levelFull}} - <i>{{c.teacher}}</i></div>
|
||||
<div class="info">
|
||||
<span class="name">{{c.name}} : </span>
|
||||
<span class="classes"><i class="el-icon-s-home"/> {{c.classes.length}}</span>
|
||||
<span class="enrollments"><i class="el-icon-user"/> {{c.enrollments}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="float-right">
|
||||
<LoadingSpinner v-if="c.rating == null" class="loading" size="30" :centered="false"/>
|
||||
<div v-else class="rating">
|
||||
<span v-if="c.rating.totalCount === 0" class="text">No ratings yet...</span>
|
||||
<span v-else class="stars">
|
||||
<StarRating :score="c.rating.overallRating"></StarRating>
|
||||
<span class="info">
|
||||
<span class="numeric-rating">{{c.rating.overallRating.toFixed(2)}} / 5</span>
|
||||
<span>({{c.rating.totalCount}} rating{{c.rating.totalCount > 1?'s':''}})</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Detail / Comments Popup -->
|
||||
<el-dialog id="detail-popup" v-if="detailsCourse" :visible="detailsCourse != null" width="50%" top="10vh"
|
||||
:before-close="closeDetails">
|
||||
<span slot="title" class="header">
|
||||
<div class="title">Ratings for {{detailsCourse.name}}</div>
|
||||
<span class="subtitle">And for {{detailsCourse.teacher}}</span>
|
||||
</span>
|
||||
|
||||
<div class="rating-item" v-for="(criteria, index) of ratingCriteria">
|
||||
<div class="title float-left">{{criteria.title}}</div>
|
||||
|
||||
<div class="stars float-right">
|
||||
<span class="info numeric-rating">{{rating.ratingAverages[index].toFixed(2)}} / 5</span>
|
||||
<StarRating :score="rating.ratingAverages[index]" style="display: inline-block"></StarRating>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="comments">
|
||||
<div class="header">
|
||||
Comments
|
||||
</div>
|
||||
|
||||
<LoadingSpinner v-if="detailsComments == null"/>
|
||||
<div class="comment" v-else v-for="comment of detailsComments">
|
||||
<div class="user">
|
||||
<i class="el-icon-user-solid"/>
|
||||
{{comment.firstName}} {{comment.lastName}}:
|
||||
<span class="info numeric-rating" style="margin-left: 5px">{{comment.averageRating.toFixed(2)}} / 5</span>
|
||||
</div>
|
||||
<div class="text">
|
||||
<blockquote>{{comment.comment}}</blockquote>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator'
|
||||
import CourseInfo, {AnalyzedRating, CourseInfoRating, RATING_CRITERIA, UniqueCourse} from '@/logic/course-info';
|
||||
import App from '@/components/app/app';
|
||||
import course from '@/logic/course';
|
||||
import LoadingSpinner from '@/components/loading-spinner.vue';
|
||||
import loading from '@/components/overlays/loading.vue';
|
||||
import StarRating from '@/components/star-rating.vue';
|
||||
@Component({components: {StarRating, LoadingSpinner}})
|
||||
export default class CourseDetail extends Vue
|
||||
{
|
||||
@Prop({required: true}) uniqueCourse: UniqueCourse;
|
||||
|
||||
detailsCourse: CourseInfo = null as any as CourseInfo
|
||||
detailsComments: CourseInfoRating[] = null as any as []
|
||||
|
||||
get ratingCriteria() {return RATING_CRITERIA}
|
||||
get rating() {return this.detailsCourse.rating}
|
||||
|
||||
mounted()
|
||||
{
|
||||
this.checkRatings()
|
||||
}
|
||||
|
||||
updated()
|
||||
{
|
||||
this.checkRatings()
|
||||
}
|
||||
|
||||
checkRatings()
|
||||
{
|
||||
// Load ratings
|
||||
this.sortedCourses.forEach(c =>
|
||||
{
|
||||
// Already has rating
|
||||
if (c.rating as any != null) return;
|
||||
|
||||
// Get rating
|
||||
App.http.post('/course-info/rating/get', {condition: 'course', value: c.id_ci}).then(result =>
|
||||
{
|
||||
if (result.success)
|
||||
{
|
||||
// Assign rating
|
||||
c.rating = new AnalyzedRating(result.data);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.$message.error(`Rating data for ${c.name} / ${c.teacher} failed to load.`)
|
||||
console.log(result.data);
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
get sortedCourses(): CourseInfo[]
|
||||
{
|
||||
return this.uniqueCourse.courses.sort((a, b) => a.levelID - b.levelID);
|
||||
}
|
||||
|
||||
openDetails(course: CourseInfo)
|
||||
{
|
||||
let c = this.detailsCourse = this.detailsCourse == course ? null as any as CourseInfo : course;
|
||||
|
||||
// Load comments
|
||||
App.http.post('/course-info/rating/get', {condition: 'course-comments', value: c.id_ci}).then(result =>
|
||||
{
|
||||
if (result.success)
|
||||
{
|
||||
this.detailsComments = result.data.map((r:any) => new CourseInfoRating(r));
|
||||
}
|
||||
else
|
||||
{
|
||||
this.$message.error(`Rating data for ${c.name} / ${c.teacher} failed to load.`)
|
||||
console.log(result.data);
|
||||
}
|
||||
})
|
||||
|
||||
// TODO: Finish comment section
|
||||
}
|
||||
|
||||
closeDetails()
|
||||
{
|
||||
this.detailsCourse = null as any as CourseInfo;
|
||||
this.detailsComments = null as any as []
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style src="./pages.scss" lang="scss" scoped/>
|
||||
<style lang="scss" scoped>
|
||||
.item
|
||||
{
|
||||
text-align: left;
|
||||
margin-bottom: 15px;
|
||||
background: #f8fdff;
|
||||
border-radius: 4px;
|
||||
padding: 5px 10px;
|
||||
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.info
|
||||
{
|
||||
font-size: 12px;
|
||||
color: gray;
|
||||
|
||||
.classes
|
||||
{
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.numeric-rating
|
||||
{
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.float-left
|
||||
{
|
||||
text-align: left;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.float-right
|
||||
{
|
||||
text-align: right;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.loading
|
||||
{
|
||||
margin-top: 5px !important;
|
||||
}
|
||||
|
||||
.rating
|
||||
{
|
||||
.text
|
||||
{
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
#detail-popup
|
||||
{
|
||||
text-align: left;
|
||||
|
||||
.header
|
||||
{
|
||||
.title
|
||||
{
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-bottom: -10px;
|
||||
}
|
||||
|
||||
.subtitle
|
||||
{
|
||||
margin-left: 10px;
|
||||
font-size: 12px;
|
||||
font-style: italic;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
|
||||
.rating-item
|
||||
{
|
||||
height: 30px;
|
||||
|
||||
.title
|
||||
{
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.rating-item:first-child
|
||||
{
|
||||
margin-top: -15px;
|
||||
}
|
||||
|
||||
.comments
|
||||
{
|
||||
margin-top: 40px;
|
||||
|
||||
.comment
|
||||
{
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
blockquote
|
||||
{
|
||||
padding: 0 1em;
|
||||
/* color: #6a737d; */
|
||||
border-left: .25em solid #dfe2e5;
|
||||
margin: 5px 0 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,16 @@
|
||||
.header
|
||||
{
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
font-size: 24px;
|
||||
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.divider
|
||||
{
|
||||
margin-top: 20px !important;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<div id="settings">
|
||||
<div class="header">Settings</div>
|
||||
<el-divider class="divider"><i class="el-icon-s-tools"></i></el-divider>
|
||||
|
||||
<div class="content">
|
||||
<el-switch v-model="settings.showAllCourses" class="item"
|
||||
active-text="Show all courses (including the ones not listed on your grade level)"/>
|
||||
|
||||
<div class="item">
|
||||
<span class="item-label">Sort by:</span>
|
||||
<el-radio-group v-model="settings.sortBy">
|
||||
<el-radio label="Name"></el-radio>
|
||||
<el-radio label="Popularity"></el-radio>
|
||||
<el-radio label="Classes"></el-radio>
|
||||
<el-radio label="Level"></el-radio>
|
||||
<el-radio disabled label="Peer Rating (Coming soon)"></el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
|
||||
<div class="item">
|
||||
<span class="item-label">Levels:</span>
|
||||
<el-checkbox-group v-model="settings.levels" style="display: inline-block;">
|
||||
<el-checkbox label="AP">AP</el-checkbox>
|
||||
<el-checkbox label="H">Honors</el-checkbox>
|
||||
<el-checkbox label="A">Accelerated</el-checkbox>
|
||||
<el-checkbox label="CP">CP</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator'
|
||||
import App from '@/components/app/app';
|
||||
|
||||
@Component
|
||||
export default class SearchSettingsComponent extends Vue
|
||||
{
|
||||
@Prop({required: true}) settings: SearchSettings;
|
||||
// TODO: Show all courses option
|
||||
}
|
||||
|
||||
export class SearchSettings
|
||||
{
|
||||
showAllCourses: boolean = App.instance.user.gradeLevel == 12;
|
||||
sortBy: string = 'Name'
|
||||
levels: string[] = ['AP','H','A','CP']
|
||||
}
|
||||
</script>
|
||||
|
||||
<style src="./pages.scss" lang="scss" scoped/>
|
||||
<style lang="scss" scoped>
|
||||
.content
|
||||
{
|
||||
text-align: left;
|
||||
|
||||
.item
|
||||
{
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
|
||||
.item-label,.el-radio,.el-checkbox
|
||||
{
|
||||
margin-right: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div id="welcome">
|
||||
<div class="header">Welcome</div>
|
||||
<el-divider class="divider"><i class="el-icon-cold-drink"></i></el-divider>
|
||||
|
||||
<div class="content" style="color: #ff3d3d" v-if="app.user.gradeLevel >= 12">
|
||||
You are a senior, what are you doing over here lol. <br>
|
||||
Unfortunately I can't help you with college course selection.<br>
|
||||
(But you can still view course ratings)<br><br>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<span style="color:#409EFF">
|
||||
This new page is designed to help you with your course selection for your {{nextGrade}} year,
|
||||
providing more information such as how many people are currently enrolled in a course.
|
||||
</span>
|
||||
<br><br>
|
||||
The courses displayed are from the current year,
|
||||
but since they are unlikely to change,
|
||||
they can provide a good view for the courses next year.
|
||||
However, this also means that the new courses
|
||||
and courses that didn't open this year are not going to be displayed here.
|
||||
For 2020, the new courses are Financial Algebra and Acc Psychology.
|
||||
Also, by default, only the courses that current {{nextGrade.toLowerCase()}}s take are displayed,
|
||||
and you can enable "show all courses" in settings if you want to see all courses.
|
||||
<br><br>
|
||||
<b>Notations:</b><br>
|
||||
<i class="el-icon-s-home"/>: How many classes (blocks) did the course open this year.<br>
|
||||
<i class="el-icon-user-solid"/>: How many teachers are teaching this course.<br>
|
||||
<i class="el-icon-user"/> How many students are enrolled.<br>
|
||||
<br>
|
||||
<b>Sorting:</b><br>
|
||||
By default the courses are sorted by name,
|
||||
but you can change the settings to sort by popularity, by classes, or by level.
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator'
|
||||
import App from '@/components/app/app';
|
||||
import {GPAUtils} from '@/logic/utils/gpa-utils';
|
||||
|
||||
@Component
|
||||
export default class Welcome extends Vue
|
||||
{
|
||||
@Prop({required: true}) app: App
|
||||
|
||||
get nextGrade()
|
||||
{
|
||||
return GPAUtils.gradeLevelName(this.app.user.gradeLevel + 1)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style src="./pages.scss" lang="scss" scoped/>
|
||||
<style lang="scss" scoped>
|
||||
.content
|
||||
{
|
||||
text-align: justify;
|
||||
color: #585858;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<div id="assignment-type-head">
|
||||
<el-card :body-style="{padding: '0px'}">
|
||||
<div id="type-info-card">
|
||||
<span id="type-name">{{type.name}}</span>
|
||||
<span class="type-average" v-if="type.graded">Average: {{type.percent}}%</span>
|
||||
<span class="type-average" v-if="!type.graded">No grades yet!</span>
|
||||
</div>
|
||||
|
||||
<AssignmentEntry v-for="(assignment, index) of filteredAssignments" :key="assignment.id"
|
||||
:assignment="assignment" :unread="false"
|
||||
:backgroundColor="index % 2 === 1 ? '#ffffff' : '#f7f7f7'" narrow="true">
|
||||
</AssignmentEntry>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
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}) type: AssignmentType;
|
||||
@Prop({required: true}) assignments: Assignment[];
|
||||
|
||||
get filteredAssignments()
|
||||
{
|
||||
// Filter assignments to only this type
|
||||
return this.assignments.filter(a => a.typeId == this.type.id);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
#assignment-type-head
|
||||
{
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#type-info-card
|
||||
{
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
#type-name
|
||||
{
|
||||
// Font
|
||||
font-size: 22px;
|
||||
color: var(--main);
|
||||
|
||||
// Center
|
||||
height: 60px;
|
||||
line-height: 60px;
|
||||
|
||||
// Alignment
|
||||
padding-left: 20px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.type-average
|
||||
{
|
||||
// Font
|
||||
font-size: 14px;
|
||||
color: #8db3e4;
|
||||
|
||||
// Center
|
||||
height: 60px;
|
||||
line-height: 64px;
|
||||
|
||||
// Alignment
|
||||
float: left;
|
||||
margin-left: 15px;
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,26 @@
|
||||
// Card
|
||||
.el-card.course-card
|
||||
{
|
||||
// Margins
|
||||
margin-right: 20px;
|
||||
margin-left: 20px;
|
||||
|
||||
// Limit name length
|
||||
white-space: nowrap;
|
||||
|
||||
// Expansion color
|
||||
background: #f4f6f9;
|
||||
}
|
||||
|
||||
.course-card-content.expand
|
||||
{
|
||||
// Top shadow
|
||||
// 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;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<el-card id="course-card" class="course-card">
|
||||
<course-head :clickable="false" :course="course" :unread="countUnread()"/>
|
||||
|
||||
<div class="course-card-content expand">
|
||||
<el-row>
|
||||
<el-col :span="24" class="course-page-graph">
|
||||
<el-card class="large overall-line-card vertical-center">
|
||||
<course-scatter :course="course"/>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
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, {Assignment} from '@/logic/course';
|
||||
import TypeRadar from '@/pages/course/type-radar/type-radar';
|
||||
import TypePie from '@/pages/course/type-pie/type-pie';
|
||||
|
||||
@Component({
|
||||
components: {TypeRadar, TypePie, AssignmentEntry, CourseHead, CourseScatter, AssignmentTypeHead}
|
||||
})
|
||||
export default class CoursePage extends Vue
|
||||
{
|
||||
@Prop({required: true}) course: Course;
|
||||
|
||||
private unread: number = -1;
|
||||
private unreadAssignments: Assignment[] = [];
|
||||
|
||||
/**
|
||||
* Count the number of unread assignments with cache
|
||||
*/
|
||||
countUnread(): number
|
||||
{
|
||||
if (this.unread == -1)
|
||||
{
|
||||
this.unreadAssignments = this.course.assignments.filter(a => a.unread);
|
||||
return this.unread = this.unreadAssignments.length;
|
||||
}
|
||||
else return this.unread;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style src="./course-page.scss" lang="scss" scoped/>
|
||||
@@ -0,0 +1,170 @@
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import Constants from '@/constants';
|
||||
import {FormatUtils} from '@/logic/utils/format-utils';
|
||||
import moment, {min, Moment} from 'moment';
|
||||
import Course, {Assignment} from '@/logic/course';
|
||||
import GraphUtils from '@/logic/utils/graph-utils';
|
||||
import chroma from 'chroma-js';
|
||||
import Navigation from '@/components/navigation/navigation';
|
||||
|
||||
@Component
|
||||
export default class CourseScatter 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 term = Navigation.instance.getSelectedTerm()
|
||||
|
||||
// Create settings
|
||||
let settings =
|
||||
{
|
||||
// Base settings
|
||||
...GraphUtils.getBaseSettings('Assignments', 'Assignment scores for ' + this.course.name),
|
||||
|
||||
// X axis represents course names
|
||||
xAxis:
|
||||
{
|
||||
type: 'time',
|
||||
axisLabel:
|
||||
{
|
||||
formatter: (name: any) => moment(name).format('MMM DD')
|
||||
},
|
||||
min: Constants.TERMS[term == -1 ? 0 : term].getTime(),
|
||||
max: term == -1 ? moment.min(moment(), moment(Constants.TERMS[4])).toDate().getTime() :
|
||||
Constants.TERMS[term + 1].getTime()
|
||||
},
|
||||
|
||||
// Y axis represents GPAs and MaxGPAs
|
||||
yAxis:
|
||||
{
|
||||
type: 'value',
|
||||
name: 'Percentage Score',
|
||||
nameLocation: 'center',
|
||||
nameGap: 38,
|
||||
axisLabel:
|
||||
{
|
||||
formatter: (name: any) => name + '%'
|
||||
},
|
||||
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[]) => 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
|
||||
legend:
|
||||
{
|
||||
bottom: 24,
|
||||
itemWidth: 14,
|
||||
textStyle:
|
||||
{
|
||||
color: '#777',
|
||||
fontSize: 11
|
||||
}
|
||||
},
|
||||
|
||||
// Data
|
||||
series: this.series()
|
||||
};
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get series data
|
||||
*/
|
||||
private series()
|
||||
{
|
||||
// Create scatter plots
|
||||
let series: any[] = this.course.assignmentTypes.filter(t => t.graded).map((type, i) =>
|
||||
{
|
||||
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),
|
||||
|
||||
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]}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Push other stuff
|
||||
series.push(
|
||||
{
|
||||
type: 'line',
|
||||
markLine: GraphUtils.getTermLines(),
|
||||
markArea: GraphUtils.getGradeMarkAreas(0.4)
|
||||
});
|
||||
|
||||
return series;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert assignments to series data
|
||||
*
|
||||
* @param assignments Assignments
|
||||
*/
|
||||
private static assignmentsData(assignments: Assignment[])
|
||||
{
|
||||
return assignments.filter(a => a.complete == 'Complete')
|
||||
.map(a => [a.time, (a.score / a.scoreMax * 100).toFixed(2), a]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<div id="course-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>
|
||||
@@ -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.filter(t => t.graded).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.filter(t => t.graded).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.filter(t => t.graded).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.filter(t => t.graded).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,7 +0,0 @@
|
||||
#graph-average
|
||||
{
|
||||
.graph
|
||||
{
|
||||
margin-top: 50px;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<template>
|
||||
<div id="graph-average">
|
||||
<ve-bar height="450px" class="graph"
|
||||
:extend="chartSettings"></ve-bar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./graph-average.ts" lang="ts"></script>
|
||||
<style src="./graph-average.scss" lang="scss"></style>
|
||||
@@ -1,181 +0,0 @@
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import {Course} from '@/components/app/app';
|
||||
|
||||
@Component({
|
||||
})
|
||||
export default class GraphOverall extends Vue
|
||||
{
|
||||
// @ts-ignore
|
||||
@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,
|
||||
//left: 'auto',
|
||||
//align: 'left',
|
||||
//orient: 'vertical'
|
||||
textStyle:
|
||||
{
|
||||
fontSize: 11
|
||||
},
|
||||
icon: 'circle'
|
||||
},
|
||||
// Zoom bar
|
||||
dataZoom:
|
||||
[
|
||||
// TODO: Calculate real value for startValue
|
||||
{
|
||||
startValue: '9/13/2019'
|
||||
},
|
||||
{
|
||||
type: 'inside'
|
||||
}
|
||||
],
|
||||
series:
|
||||
{
|
||||
smooth: true
|
||||
},
|
||||
xAxis:
|
||||
{
|
||||
//type: 'time'
|
||||
},
|
||||
yAxis:
|
||||
{
|
||||
min: (value: any) => value.min - 10
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert assignments list to a graph dataset.
|
||||
*/
|
||||
get convertChart()
|
||||
{
|
||||
let courses = this.courses;
|
||||
|
||||
// Compute the column names
|
||||
let columns = courses.map(course => course.name);
|
||||
columns.unshift('date');
|
||||
|
||||
// Find the min date
|
||||
let minDates = courses.map(course => new Date(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))
|
||||
{
|
||||
dates.push(new Date(date));
|
||||
}
|
||||
|
||||
// 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')};
|
||||
|
||||
// Loop through courses
|
||||
courses.forEach(course =>
|
||||
{
|
||||
// Total Mean
|
||||
if (course.grading.method == 'TOTAL_MEAN')
|
||||
{
|
||||
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
|
||||
let assignmentDate = new Date(assignment.date);
|
||||
if (assignmentDate.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')
|
||||
{
|
||||
let typeScores: {[index: string]: any} = {};
|
||||
let typeCounts: {[index: string]: any} = {};
|
||||
|
||||
// Loop through assignments
|
||||
course.assignments.forEach(assignment =>
|
||||
{
|
||||
// If assignment should be displayed
|
||||
if (assignment.complete != 'Complete') return;
|
||||
|
||||
// Date is being looked at
|
||||
let assignmentDate = new Date(assignment.date);
|
||||
if (assignmentDate.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);
|
||||
});
|
||||
|
||||
console.log(rows);
|
||||
|
||||
return {
|
||||
columns: columns,
|
||||
rows: rows
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
<template>
|
||||
<div id="graph-overall">
|
||||
<ve-line :data="convertChart" :extend="settings"></ve-line>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./graph-overall.ts" lang="ts"></script>
|
||||
<style src="./graph-overall.scss" lang="scss"></style>
|
||||
@@ -1,13 +1,12 @@
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import {Course} from '@/components/app/app';
|
||||
import {GPAUtils} from '@/utils/gpa-utils';
|
||||
import Course from '@/logic/course';
|
||||
import {GPAUtils} from '@/logic/utils/gpa-utils';
|
||||
import Constants from '@/constants';
|
||||
import {FormatUtils} from '@/logic/utils/format-utils';
|
||||
|
||||
@Component({
|
||||
})
|
||||
export default class GraphAverage extends Vue
|
||||
@Component
|
||||
export default class OverallBar extends Vue
|
||||
{
|
||||
// @ts-ignore
|
||||
@Prop({required: true}) courses: Course[];
|
||||
|
||||
/**
|
||||
@@ -40,7 +39,7 @@ export default class GraphAverage extends Vue
|
||||
rotate: 90,
|
||||
|
||||
// Truncate text length
|
||||
formatter: (value: string) => value.length <= 16 ? value : value.substr(0, 14) + '...'
|
||||
formatter: (value: string) => FormatUtils.limit(value, 16)
|
||||
},
|
||||
},
|
||||
|
||||
@@ -53,15 +52,16 @@ export default class GraphAverage extends Vue
|
||||
// Data
|
||||
series:
|
||||
[
|
||||
// Max GP
|
||||
{
|
||||
type: 'bar',
|
||||
barGap: '-100%',
|
||||
data: this.courses.map(course =>
|
||||
{
|
||||
return {value: [course.name, GPAUtils.getGP(course, 'A+')],
|
||||
itemStyle: {color: '#d8d8d8'}}
|
||||
return {value: [course.name, GPAUtils.getGP(course, 'A+')], itemStyle: {color: '#d8d8d8'}}
|
||||
}),
|
||||
},
|
||||
// Current GP
|
||||
{
|
||||
type: 'bar',
|
||||
barGap: '-100%',
|
||||
@@ -82,9 +82,6 @@ export default class GraphAverage extends Vue
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: Remove this
|
||||
console.log(settings);
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
@@ -95,14 +92,21 @@ export default class GraphAverage extends Vue
|
||||
{
|
||||
let data: any = [];
|
||||
|
||||
this.courses.forEach(course =>
|
||||
this.courses.forEach((course, index) =>
|
||||
{
|
||||
// Get GP
|
||||
let gp = GPAUtils.getGP(course, course.letterGrade);
|
||||
|
||||
// No grade cases
|
||||
if (gp == -1) return;
|
||||
|
||||
// Push data
|
||||
data.push(
|
||||
{
|
||||
value: [course.name, GPAUtils.getGP(course, course.letterGrade)],
|
||||
value: [course.name, gp],
|
||||
itemStyle:
|
||||
{
|
||||
color: Constants.THEME.colors[data.length]
|
||||
color: Constants.THEME.colors[index]
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<div id="overall-bar">
|
||||
<ve-bar height="450px" class="graph" :extend="chartSettings"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./overall-bar.ts" lang="ts"></script>
|
||||
<style lang="scss" scoped>
|
||||
#overall-bar
|
||||
{
|
||||
.graph
|
||||
{
|
||||
margin-top: 50px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,152 @@
|
||||
|
||||
// Row
|
||||
.assignment-entry
|
||||
{
|
||||
height: 40px;
|
||||
padding: 0 10px 0 20px;
|
||||
background: #f5f7fa;
|
||||
|
||||
text-align: left;
|
||||
|
||||
// Date
|
||||
.el-col.date
|
||||
{
|
||||
min-width: 150px;
|
||||
|
||||
span.month
|
||||
{
|
||||
margin-right: 5px;
|
||||
|
||||
// Unified width
|
||||
display: inline-block;
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
span.now
|
||||
{
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
}
|
||||
}
|
||||
|
||||
// Description
|
||||
.el-col.description
|
||||
{
|
||||
width: unset;
|
||||
|
||||
span.type
|
||||
{
|
||||
display: inline-block;
|
||||
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
|
||||
background: #eee;
|
||||
border-left: 2px solid #000;
|
||||
|
||||
height: 22px;
|
||||
line-height: 22px;
|
||||
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
// Grade
|
||||
.el-col.grade
|
||||
{
|
||||
text-align: right;
|
||||
float: right;
|
||||
|
||||
// Fix smaller screen display issues.
|
||||
width: unset;
|
||||
|
||||
// Status / Problems
|
||||
span.status
|
||||
{
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
// Percentage score
|
||||
span.percent
|
||||
{
|
||||
font-style: italic;
|
||||
background: #ffc;
|
||||
color: #555;
|
||||
|
||||
margin-right: 8px;
|
||||
|
||||
.symbol
|
||||
{
|
||||
padding: 0 1px;
|
||||
}
|
||||
}
|
||||
|
||||
// Score you got
|
||||
span.score
|
||||
{
|
||||
background: #f2f2f2;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
// Max score
|
||||
span.max
|
||||
{
|
||||
background: #ddd;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
// Mark as read
|
||||
button.mark-as-read
|
||||
{
|
||||
margin-left: 8px;
|
||||
color: #aaa;
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.entry-box
|
||||
{
|
||||
height: 22px;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
// Unified width
|
||||
.entry-box.score, .entry-box.max
|
||||
{
|
||||
min-width: 30px;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
// Unified width
|
||||
.entry-box.percent
|
||||
{
|
||||
min-width: 60px;
|
||||
display: inline-block;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
// Narrow layout
|
||||
.assignment-entry.narrow
|
||||
{
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
// Unread
|
||||
.no-unread
|
||||
{
|
||||
visibility: hidden !important;
|
||||
width: 0 !important;
|
||||
margin-left: 0 !important;
|
||||
padding: 0 0 0 10px !important;
|
||||
}
|
||||
|
||||
.assignment-entry:first-child
|
||||
{
|
||||
padding-top: 3px;
|
||||
|
||||
// Top shadow
|
||||
// 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);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<div class="assignment-entry vertical-center"
|
||||
:class="narrow ? 'narrow' : ''"
|
||||
:style="`background: ${backgroundColor}`">
|
||||
|
||||
<el-row class="unread-row">
|
||||
<el-col :span="3" class="date">
|
||||
<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">
|
||||
<span class="type entry-box"
|
||||
:style="`border-color: var(--assignment-type-${assignment.typeId})`">
|
||||
{{assignment.type}}
|
||||
</span>
|
||||
<span class="text">{{assignment.description}}</span>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="6" class="grade">
|
||||
<span v-if="assignment.problem" class="status entry-box" :style="{color: assignment.problemColor}">
|
||||
{{assignment.problem}}
|
||||
</span>
|
||||
<span v-if="assignment.graded" class="percent entry-box">
|
||||
{{(assignment.score / assignment.scoreMax * 100).toFixed(1)}}
|
||||
<span class="symbol">%</span>
|
||||
</span>
|
||||
<span v-if="assignment.graded" class="score entry-box">{{assignment.score}}</span>
|
||||
<span v-if="assignment.graded" class="max entry-box">{{assignment.scoreMax}}</span>
|
||||
|
||||
<el-button class="mark-as-read" :class="unread ? 'unread' : 'no-unread'"
|
||||
size="mini" type="text" icon="el-icon-close"
|
||||
@click="markAsRead">
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import moment from 'moment';
|
||||
import {Assignment} from '@/logic/course';
|
||||
|
||||
@Component
|
||||
export default class AssignmentEntry extends Vue
|
||||
{
|
||||
@Prop({required: true}) assignment: Assignment;
|
||||
|
||||
@Prop({default: false}) unread: boolean;
|
||||
@Prop({default: '#f5f7fa'}) backgroundColor: string;
|
||||
@Prop({default: false}) narrow: boolean;
|
||||
|
||||
/**
|
||||
* Format a date to the displayed format
|
||||
*
|
||||
* @param date Date
|
||||
*/
|
||||
getMoment(date: number)
|
||||
{
|
||||
return moment(new Date(date));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark this unread assignment as read
|
||||
*/
|
||||
markAsRead()
|
||||
{
|
||||
// Call custom event
|
||||
this.$emit('mark-as-read', this.assignment)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style src="./assignment-entry.scss" lang="scss" scoped/>
|
||||
@@ -0,0 +1,216 @@
|
||||
// Main card content
|
||||
.course-card-content.main
|
||||
{
|
||||
// Main color
|
||||
background: white;
|
||||
|
||||
// Alignment
|
||||
display: block;
|
||||
|
||||
padding: 20px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
#block-info
|
||||
{
|
||||
// Align left
|
||||
text-align: left;
|
||||
float: left;
|
||||
|
||||
#name
|
||||
{
|
||||
overflow: hidden;
|
||||
font-size: 22px;
|
||||
color: var(--main);
|
||||
}
|
||||
|
||||
#teacher
|
||||
{
|
||||
font-size: 12px;
|
||||
color: #999999;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
#block-grade
|
||||
{
|
||||
// Align right
|
||||
text-align: right;
|
||||
float: right;
|
||||
|
||||
// Adjust position
|
||||
margin-top: -2px;
|
||||
margin-left: 10px;
|
||||
|
||||
#grade
|
||||
{
|
||||
font-size: 21px;
|
||||
}
|
||||
|
||||
#updates
|
||||
{
|
||||
font-size: 14px;
|
||||
|
||||
#unread-number
|
||||
{
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
border-radius: 5px;
|
||||
|
||||
padding-left: 3px;
|
||||
padding-right: 3px;
|
||||
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
#unread-text
|
||||
{
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
#updates.unread
|
||||
{
|
||||
#unread-number
|
||||
{
|
||||
background: var(--unread);
|
||||
color: white;
|
||||
}
|
||||
|
||||
#unread-text
|
||||
{
|
||||
color: var(--unread);
|
||||
}
|
||||
}
|
||||
|
||||
#updates.none
|
||||
{
|
||||
color: #999999;
|
||||
#unread-number
|
||||
{
|
||||
background: #eeeeee;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#content
|
||||
{
|
||||
margin-top: -20px;
|
||||
padding: 20px 0 20px 20px;
|
||||
height: 50px;
|
||||
margin-left: -20px;
|
||||
}
|
||||
|
||||
#block-rate
|
||||
{
|
||||
// Align right
|
||||
width: 55px;
|
||||
float: right;
|
||||
|
||||
margin-left: 20px;
|
||||
padding: 0 10px;
|
||||
|
||||
color: white;
|
||||
background: #84c0ff;
|
||||
border-radius: 0 4px 4px 0;
|
||||
|
||||
height: 90px;
|
||||
margin-top: -20px;
|
||||
margin-right: -20px;
|
||||
|
||||
box-shadow: inset 8px 0 11px -4px rgba(0,0,0,.1);
|
||||
}
|
||||
|
||||
#block-rate.rated
|
||||
{
|
||||
background: #66eaad;
|
||||
}
|
||||
|
||||
.dark #block-rate > span
|
||||
{
|
||||
color: #84c0ff !important;
|
||||
}
|
||||
|
||||
.dark #block-rate.rated > span
|
||||
{
|
||||
color: #66eaad !important;
|
||||
}
|
||||
|
||||
#rating-popup
|
||||
{
|
||||
text-align: left;
|
||||
|
||||
.header
|
||||
{
|
||||
.title
|
||||
{
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.subtitle
|
||||
{
|
||||
margin-left: 10px;
|
||||
font-size: 12px;
|
||||
font-style: italic;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
|
||||
.item
|
||||
{
|
||||
margin-bottom: 10px;
|
||||
white-space: normal;
|
||||
|
||||
.title
|
||||
{
|
||||
font-size: 18px;
|
||||
color: var(--main);
|
||||
}
|
||||
|
||||
.description
|
||||
{
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.stars
|
||||
{
|
||||
font-size: 20px;
|
||||
color: #FFB300;
|
||||
}
|
||||
|
||||
.el-textarea
|
||||
{
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.item:first-child
|
||||
{
|
||||
margin-top: -15px;
|
||||
}
|
||||
}
|
||||
|
||||
#block-term-grades
|
||||
{
|
||||
// Align right
|
||||
width: auto;
|
||||
float: right;
|
||||
margin-right: 10px;
|
||||
|
||||
color: gray;
|
||||
|
||||
#term, #term-numeric
|
||||
{
|
||||
font-size: 11px;
|
||||
color: #b3b3b3;
|
||||
}
|
||||
|
||||
#term-letter
|
||||
{
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
<template>
|
||||
<div id="course-head" class="course-card-content main vertical-center">
|
||||
|
||||
<!-- Rating button -->
|
||||
<div id="block-rate" v-if="displayRate" class="vertical-center clickable"
|
||||
@click="ratingDialog = true" :class="{rated: course.rated}">
|
||||
<span v-if="!course.rated">Give a<br>Rating!</span>
|
||||
<span v-else>Rating<br>Entered</span>
|
||||
</div>
|
||||
|
||||
<!-- Main content -->
|
||||
<div id="content" @click="redirect" :class="clickable ? 'clickable' : ''">
|
||||
|
||||
<!-- Left -->
|
||||
<div id="block-info">
|
||||
<div id="name">{{course.name}}</div>
|
||||
<div id="teacher">{{course.teacherName}}</div>
|
||||
</div>
|
||||
|
||||
<!-- Right -->
|
||||
<div id="block-grade">
|
||||
<div id="grade">
|
||||
<span id="letter">{{course.letterGrade}} </span>
|
||||
<span id="numeric">{{course.numericGrade === undefined ? '--' : course.numericGrade.toFixed(2)}}</span>
|
||||
<span id="percent" v-if="course.numericGrade !== undefined">%</span>
|
||||
</div>
|
||||
<div id="updates" @click="redirect" :class="unread === 0 ? 'none' : 'unread'">
|
||||
<span id="unread-number">{{unread}}</span>
|
||||
<span id="unread-text" :class="clickable ? 'clickable' : ''">new update{{unread >= 2 ? 's' : ''}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="block-term-grades" v-if="course.rawSelectedTerm === -1"
|
||||
v-for="term in course.allGradingPeriods.slice().reverse()">
|
||||
<div id="term">Term {{term + 1}}</div>
|
||||
<div id="term-letter">{{course.letterGradeTerm(term)}}</div>
|
||||
<div id="term-numeric">{{course.numericGradeTerm(term).toFixed(1)}}%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rating Popup -->
|
||||
<el-dialog id="rating-popup" :visible.sync="ratingDialog" width="50%" top="5vh"
|
||||
:show-close="false" :close-on-click-modal="false" :close-on-press-escape="false">
|
||||
|
||||
<span slot="title" class="header">
|
||||
<div class="title">Give a Rating for {{course.name}}</div>
|
||||
<span class="subtitle">And for {{course.teacherName}}<br></span>
|
||||
<span class="subtitle" style="color: #e67b0d;">(might need to scroll down to find the submit button)</span>
|
||||
</span>
|
||||
|
||||
<div class="item" v-for="(criteria, index) of ratingCriteria">
|
||||
<div class="title">{{criteria.title}}</div>
|
||||
<div class="description">{{criteria.desc}}</div>
|
||||
|
||||
<div class="stars">
|
||||
<span class="star clickable" v-for="star in [0,1,2,3,4]" @click="changeStars(index, star)">
|
||||
<i v-if="rating.ratings[index] > star" class="el-icon-star-on"/>
|
||||
<i v-else class="el-icon-star-off"/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item">
|
||||
<div class="title">Comments</div>
|
||||
<div class="description">Any additional comments? (this is optional)</div>
|
||||
|
||||
<el-input type="textarea" placeholder="Comments... (Optional)"
|
||||
v-model="rating.comment" maxlength="4500" show-word-limit :autosize="{minRows: 2, maxRows: 4}">
|
||||
</el-input>
|
||||
<el-checkbox v-model="rating.anonymous">Anonymous</el-checkbox>
|
||||
</div>
|
||||
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="ratingDialog = false" :disabled="ratingPosting">Cancel</el-button>
|
||||
<el-button type="primary" @click="submitRating()" :disabled="canSubmit">
|
||||
{{course.rated ? 'Update' : 'Submit'}}
|
||||
</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import Course from '@/logic/course';
|
||||
import App from '@/components/app/app';
|
||||
import {GPAUtils} from '@/logic/utils/gpa-utils';
|
||||
import Constants from '@/constants';
|
||||
import {RATING_CRITERIA} from '@/logic/course-info';
|
||||
import Navigation from '@/components/navigation/navigation';
|
||||
|
||||
@Component
|
||||
export default class CourseHead extends Vue
|
||||
{
|
||||
@Prop({required: true}) unread: number;
|
||||
@Prop({required: true}) course: Course;
|
||||
@Prop({required: true}) clickable: boolean;
|
||||
|
||||
ratingDialog = false;
|
||||
ratingPosting = false;
|
||||
rating = this.course.rating;
|
||||
|
||||
get canSubmit()
|
||||
{
|
||||
return this.ratingPosting || App.instance.demoMode
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to the course page
|
||||
*/
|
||||
redirect()
|
||||
{
|
||||
if (!this.clickable) return;
|
||||
App.instance.nav.updateIndex(this.course.urlIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display rate button or not
|
||||
*/
|
||||
get displayRate()
|
||||
{
|
||||
return this.clickable && App.instance.showRating;
|
||||
}
|
||||
|
||||
get ratingCriteria() {return RATING_CRITERIA}
|
||||
|
||||
/**
|
||||
* Change star rating data
|
||||
*
|
||||
* @param index Index of the rating
|
||||
* @param star Change to how many stars
|
||||
*/
|
||||
changeStars(index: number, star: number)
|
||||
{
|
||||
this.rating.ratings[index] = star + 1;
|
||||
this.$forceUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit a rating
|
||||
*/
|
||||
submitRating()
|
||||
{
|
||||
if (this.rating.ratings.includes(0))
|
||||
{
|
||||
this.$message.error('You haven\'t rated all of the criteria yet!');
|
||||
return;
|
||||
}
|
||||
|
||||
this.ratingPosting = true;
|
||||
|
||||
App.http.post('/course-info/rating/set', {rating: this.rating}).then(response =>
|
||||
{
|
||||
if (response.success)
|
||||
{
|
||||
this.ratingDialog = false;
|
||||
this.ratingPosting = false;
|
||||
this.$message.success('Rating successfully posted, thank you!');
|
||||
|
||||
// First rating (Updating the first review doesn't count as first review)
|
||||
if (this.course.rated) return;
|
||||
this.course.rated = true;
|
||||
if (App.instance.courses.filter(c => c.rated).length == 1)
|
||||
{
|
||||
this.$alert('You can view other courses\'' +
|
||||
' ratings in the Course Selection tab, or review yours by clicking' +
|
||||
' the green button that says "Rating Entered." There are also option to turn off the rating buttons ' +
|
||||
' by clicking on your avatar on the top right corner.',
|
||||
'Thank you for submitting your fist rating!', {confirmButtonText: 'OK'}
|
||||
);
|
||||
}
|
||||
|
||||
// Last rating
|
||||
if (App.instance.courses.filter(c => c.isGraded && !c.rated).length == 0)
|
||||
{
|
||||
this.$confirm('You have rated all of your courses! Do you want to turn off the rating buttons?' +
|
||||
' (there are option to toggle them on again by clicking your avatar on the top right corner.)',
|
||||
'Thank you for submitting rating!',
|
||||
{confirmButtonText: 'Sure!', cancelButtonText: 'Nope.'}).then(() =>
|
||||
{
|
||||
// Disable rating buttons
|
||||
Navigation.instance.onAvatarMenu('switch-rating');
|
||||
this.$message.success('Rating buttons are disabled');
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this.$message.error('Sorry, but rating failed to post, please try again or email me if you continues to have issues. ' +
|
||||
'But wait! The email system isn\'t created yet... oops!. (Technical error message: ' + response.data + ')');
|
||||
this.ratingPosting = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style src="./course-head.scss" lang="scss" scoped/>
|
||||
@@ -0,0 +1,16 @@
|
||||
// Card
|
||||
.el-card.course-card
|
||||
{
|
||||
// Margins
|
||||
margin-right: 20px;
|
||||
margin-left: 20px;
|
||||
|
||||
// Height limit
|
||||
max-height: 250px;
|
||||
|
||||
// Limit name length
|
||||
white-space: nowrap;
|
||||
|
||||
// Expansion color
|
||||
background: #f4f6f9;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<div id="overall-course">
|
||||
<el-card class="course-card">
|
||||
<course-head :clickable="true" :course="course" :unread="unread()"/>
|
||||
<div class="course-card-content expand"
|
||||
v-if="unread() !== 0">
|
||||
<assignment-entry v-for="assignment in unreadAssignments()"
|
||||
:assignment="assignment"
|
||||
:key="assignment.id"
|
||||
unread="true"
|
||||
v-on:mark-as-read="assignment.markAsRead()">
|
||||
</assignment-entry>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
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, {Assignment} from '@/logic/course';
|
||||
|
||||
@Component({
|
||||
components: {AssignmentEntry, CourseHead}
|
||||
})
|
||||
export default class OverallCourse extends Vue
|
||||
{
|
||||
@Prop({required: true}) course: Course;
|
||||
|
||||
mounted()
|
||||
{
|
||||
this.unreadAssignments().forEach(a => a.addCallback(() => this.$forceUpdate()));
|
||||
}
|
||||
|
||||
unreadAssignments(): Assignment[]
|
||||
{
|
||||
return this.course.assignments.filter(a => a.unread);
|
||||
}
|
||||
|
||||
unread(): number
|
||||
{
|
||||
return this.unreadAssignments().length;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style src="./overall-course.scss" lang="scss" scoped/>
|
||||
@@ -0,0 +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
|
||||
export default class OverallLine extends Vue
|
||||
{
|
||||
@Prop({required: true}) courses: Course[];
|
||||
|
||||
filteredCourses: Course[];
|
||||
settings: any;
|
||||
|
||||
/**
|
||||
* When this component is created
|
||||
*/
|
||||
created()
|
||||
{
|
||||
// Filter courses
|
||||
this.filteredCourses = this.courses.filter(c => c.isGraded && c.assignments.length > 0);
|
||||
|
||||
// Generate settings
|
||||
this.settings =
|
||||
{
|
||||
...GraphUtils.getBaseSettings('Average Grade', 'Average score trend for every course'),
|
||||
|
||||
// Zoom bar
|
||||
dataZoom:
|
||||
[
|
||||
{
|
||||
type: 'slider',
|
||||
startValue: this.getStartDate(),
|
||||
|
||||
// Minimum zoom: 1 week
|
||||
minValueSpan: 7 * 24 * 60 * 60 * 1000
|
||||
}
|
||||
],
|
||||
|
||||
// Tooltip
|
||||
tooltip:
|
||||
{
|
||||
... GraphUtils.tooltipCssShadow(),
|
||||
|
||||
trigger: 'axis'
|
||||
},
|
||||
|
||||
// Axis
|
||||
xAxis:
|
||||
{
|
||||
type: 'time',
|
||||
axisLabel:
|
||||
{
|
||||
formatter: (name: any) => moment(name).format('MMM DD')
|
||||
},
|
||||
},
|
||||
yAxis:
|
||||
{
|
||||
axisLabel:
|
||||
{
|
||||
formatter: (name: any) => name + '%'
|
||||
},
|
||||
min: (value: any) => Math.floor(value.min),
|
||||
max: (value: any) => Math.min(Math.ceil(value.max), 110)
|
||||
},
|
||||
|
||||
// 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]];
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<div id="overall-line">
|
||||
<ve-line :extend="{a: this.courses}" :after-config="afterConfig"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./overall-line.ts" lang="ts"></script>
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,17 +1,4 @@
|
||||
|
||||
// Add some margins
|
||||
.el-card
|
||||
{
|
||||
margin: 10px;
|
||||
height: 494px;
|
||||
padding: 0;
|
||||
|
||||
// Vertical center
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.gpa-card
|
||||
{
|
||||
margin-left: 20px;
|
||||
@@ -31,7 +18,7 @@
|
||||
.gpa.text
|
||||
{
|
||||
font-size: 35px;
|
||||
font-family: 'Avenir', Helvetica, Arial, sans-serif;
|
||||
font-family: var(--font);
|
||||
}
|
||||
|
||||
.gpa.max
|
||||
@@ -47,16 +34,27 @@
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
// Graph average
|
||||
.graph-average-card
|
||||
.no-grade
|
||||
{
|
||||
font-size: 30px;
|
||||
color: #b1b1b1;
|
||||
|
||||
// Disable selecting
|
||||
display:block;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
// Cards
|
||||
.el-card.overall-bar-card
|
||||
{
|
||||
margin-right: 20px;
|
||||
min-width: 170px;
|
||||
}
|
||||
|
||||
// Fix padding
|
||||
.el-card__body
|
||||
.dialog-checkbox
|
||||
{
|
||||
padding-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
display: block;
|
||||
margin-top: 20px;
|
||||
margin-bottom: -20px;
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import GraphOverall from '@/pages/overall/graph-overall/graph-overall';
|
||||
import GraphAverage from '@/pages/overall/graph-average/graph-average';
|
||||
import {Course} from '@/components/app/app';
|
||||
import {GPAUtils} from '@/utils/gpa-utils';
|
||||
|
||||
@Component({
|
||||
components: {GraphOverall, GraphAverage}
|
||||
})
|
||||
export default class Overall extends Vue
|
||||
{
|
||||
// @ts-ignore
|
||||
@Prop({required: true}) courses: Course[];
|
||||
|
||||
/**
|
||||
* This function is called to get gpa since I can't import another
|
||||
* class in the Vue file.
|
||||
*/
|
||||
public getGPA()
|
||||
{
|
||||
return GPAUtils.getGPA(this.courses);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,22 @@
|
||||
<template>
|
||||
<div id="overall">
|
||||
<el-row>
|
||||
<el-col :span="4">
|
||||
<el-card class="gpa-card">
|
||||
<el-progress v-if="started" :text-inside="true" :percentage="progress()"
|
||||
:stroke-width="20" status="success" style="margin: 0 20px"/>
|
||||
|
||||
<el-dialog title="Notice" :visible.sync="clearUnreadPrompt" @close="clearUnread(false)"
|
||||
width="30%" style="word-break: unset;">
|
||||
<span>You have too many new grade notifications. Clear them now?</span>
|
||||
<img src="./too-many-unread.png" alt=""/>
|
||||
<el-checkbox class="dialog-checkbox" v-model="dontAskAgain">Don't Ask Again</el-checkbox>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="clearUnread(false)" style="float: left">Nope</el-button>
|
||||
<el-button type="primary" @click="clearUnread(true)">Sure!</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
|
||||
<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,20 +27,115 @@
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="14">
|
||||
<el-card>
|
||||
<graph-overall :courses="courses"></graph-overall>
|
||||
<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="graph-average-card">
|
||||
<graph-average :courses="courses"></graph-average>
|
||||
<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>
|
||||
<div class=""></div>
|
||||
|
||||
<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"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./overall.ts" lang="ts"></script>
|
||||
<style src="./overall.scss" lang="scss"></style>
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import OverallLine from '@/pages/overall/overall-line/overall-line';
|
||||
import OverallBar from '@/pages/overall/overall-bar/overall-bar';
|
||||
import OverallCourse from '@/pages/overall/overall-course/overall-course.vue';
|
||||
import Course, {Assignment} from '@/logic/course';
|
||||
import {GPAUtils} from '@/logic/utils/gpa-utils';
|
||||
|
||||
@Component({
|
||||
components: {OverallLine, OverallBar, OverallCourse}
|
||||
})
|
||||
export default class Overall extends Vue
|
||||
{
|
||||
@Prop({required: true}) courses: Course[];
|
||||
|
||||
/**
|
||||
* This function is called to get gpa since I can't import another
|
||||
* class in the Vue file.
|
||||
*/
|
||||
getGPA()
|
||||
{
|
||||
return GPAUtils.getGPA(this.courses);
|
||||
}
|
||||
|
||||
// For clear unread prompt
|
||||
unread: Assignment[];
|
||||
clearUnreadPrompt = false;
|
||||
dontAskAgain = false;
|
||||
started = false;
|
||||
|
||||
/**
|
||||
* Mark as read progress
|
||||
*/
|
||||
progress()
|
||||
{
|
||||
return +(this.unread.filter(a => !a.unread).length / this.unread.length * 100).toFixed(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* On page load - check if the user has too many notifications
|
||||
*/
|
||||
mounted()
|
||||
{
|
||||
// Check unread
|
||||
if (!this.$cookies.isKey('va.ignore-unread'))
|
||||
{
|
||||
// Count unread
|
||||
this.unread = this.courses.flatMap(c => c.assignments.filter(a => a.unread));
|
||||
|
||||
// Prompt clear
|
||||
if (this.unread.length > 15)
|
||||
{
|
||||
this.clearUnreadPrompt = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear unread
|
||||
*
|
||||
* @param confirmed
|
||||
*/
|
||||
clearUnread(confirmed: boolean)
|
||||
{
|
||||
// Hide prompt
|
||||
this.clearUnreadPrompt = false;
|
||||
|
||||
// Not confirmed, do nothing
|
||||
if (!confirmed)
|
||||
{
|
||||
if (!this.dontAskAgain) return;
|
||||
|
||||
// Don't ask again
|
||||
this.$cookies.set('va.ignore-unread', true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear unread
|
||||
this.started = true;
|
||||
this.unread.forEach((a, i) =>
|
||||
{
|
||||
// Delay: 100ms per assignment
|
||||
// I don't want my server to explode lol
|
||||
setTimeout(() => a.markAsRead().then(() => this.$forceUpdate()), 100 * i);
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style src="./overall.scss" lang="scss" scoped/>
|
||||
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -1,4 +1,4 @@
|
||||
import Vue, { VNode } from 'vue';
|
||||
import Vue, {VNode} from 'vue';
|
||||
|
||||
declare global {
|
||||
namespace JSX {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
declare module '*.vue' {
|
||||
import Vue from 'vue';
|
||||
export default Vue;
|
||||
declare module '*.vue'
|
||||
{
|
||||
import Vue from 'vue';
|
||||
export default Vue;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div id="info">
|
||||
<div id="top">
|
||||
<div id="title">Veracross Analyzer for SJP</div>
|
||||
<div id="subtitle">Know your grades better.</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Vue} from 'vue-property-decorator';
|
||||
|
||||
@Component
|
||||
export default class Info extends Vue
|
||||
{
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
#top
|
||||
{
|
||||
padding: 40vh 0;
|
||||
|
||||
// Center and scale the image nicely
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
|
||||
text-align: right;
|
||||
|
||||
#title
|
||||
{
|
||||
font-size: 30px;
|
||||
margin-right: 10vw;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#subtitle
|
||||
{
|
||||
margin-right: 10vw;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,37 +0,0 @@
|
||||
import {Course} from '@/components/app/app';
|
||||
|
||||
export class CourseUtils
|
||||
{
|
||||
/**
|
||||
* Return a list of courses that are graphed
|
||||
*
|
||||
* @param original Original course list
|
||||
* @return Course[] Filtered course list
|
||||
*/
|
||||
public static getGradedCourses(original: Course[]): Course[]
|
||||
{
|
||||
// Define result
|
||||
let result: Course[] = [];
|
||||
|
||||
// Filter through courses
|
||||
original.forEach(course =>
|
||||
{
|
||||
// Skip future or past courses
|
||||
if (course.status != 'active') return;
|
||||
|
||||
// Skip courses without levels
|
||||
if (course.level == 'None') return;
|
||||
|
||||
// Skip courses without assignments
|
||||
if (course.assignments.length == 0) return;
|
||||
|
||||
// Skip if there are no grading scale
|
||||
// if (course.grading.method == 'NOT_GRADED') return;
|
||||
|
||||
// Add it to the list
|
||||
result.push(course);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
// Initialize course specific variables
|
||||
let courseScores: {[index: string]: any} = {};
|
||||
let courseMaxScores: {[index: string]: any} = {};
|
||||
let courseIndexes: {[index: string]: any} = {};
|
||||
courses.forEach(course =>
|
||||
{
|
||||
courseScores[course.name] = 0;
|
||||
courseMaxScores[course.name] = 0;
|
||||
courseIndexes[course.name] = course.assignments.length - 1;
|
||||
});
|
||||
|
||||
// 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')};
|
||||
|
||||
// Loop through courses
|
||||
courses.forEach(course =>
|
||||
{
|
||||
// Reversed loop through the assignments
|
||||
for (let r = courseIndexes[course.name]; r >= 0; r--)
|
||||
{
|
||||
let assignment = course.assignments[r];
|
||||
|
||||
// If assignment should be displayed
|
||||
if (assignment.complete != 'Complete') continue;
|
||||
|
||||
// Date is being looked at
|
||||
let assignmentDate = new Date(assignment.date);
|
||||
if (assignmentDate.getTime() == date.getTime())
|
||||
{
|
||||
// Detect grading method and record scores
|
||||
if (course.grading.method == 'TOTAL_MEAN')
|
||||
{
|
||||
courseScores[course.name] += assignment.score;
|
||||
courseMaxScores[course.name] += assignment.scoreMax;
|
||||
}
|
||||
else if (course.grading.method == 'PERCENT_TYPE')
|
||||
{
|
||||
let scale = course.grading.weightingMap[assignment.type];
|
||||
courseScores[course.name] += assignment.score * scale;
|
||||
courseMaxScores[course.name] += assignment.scoreMax * scale;
|
||||
}
|
||||
}
|
||||
|
||||
// Not now
|
||||
else if (assignmentDate > date)
|
||||
{
|
||||
courseIndexes[course.name] = r;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Add average to the row
|
||||
row[course.name] = courseScores[course.name] / courseMaxScores[course.name] * 100;
|
||||
});
|
||||
|
||||
// Add it to the array
|
||||
rows.push(row);
|
||||
});
|
||||
|
||||
|
||||
else if (course.grading.method == 'PERCENT_TYPE')
|
||||
{
|
||||
let typeScores: {[index: string]: any} = {};
|
||||
let typeCounts: {[index: string]: any} = {};
|
||||
|
||||
// Loop through assignments
|
||||
course.assignments.forEach(assignment =>
|
||||
{
|
||||
// If assignment should be displayed
|
||||
if (assignment.complete != 'Complete') return;
|
||||
|
||||
// Date is being looked at
|
||||
let assignmentDate = new Date(assignment.date);
|
||||
if (assignmentDate.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] ++;
|
||||
}
|
||||
});
|
||||
|
||||
let score = 0;
|
||||
|
||||
// Count
|
||||
for (let type in typeScores)
|
||||
{
|
||||
score += typeScores[type] * course.grading.weightingMap[type] / typeCounts[type];
|
||||
console.log(type);
|
||||
}
|
||||
|
||||
// Add average to the row
|
||||
row[course.name] = score * 100;
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
/**
|
||||
* This is an utility class to calculate GPA.
|
||||
*/
|
||||
import {Course} from '@/components/app/app';
|
||||
|
||||
export class GPAUtils
|
||||
{
|
||||
// [[Min score, Letter grade, Base GPA], ...]
|
||||
public static SCALE =
|
||||
[
|
||||
[96.5, 'A+', 4.00],
|
||||
[92.5, 'A' , 3.75],
|
||||
[89.5, 'A-', 3.50],
|
||||
[86.5, 'B+', 3.25],
|
||||
[82.5, 'B' , 3.00],
|
||||
[79.5, 'B-', 2.75],
|
||||
[76.5, 'C+', 2.50],
|
||||
[72.5, 'C' , 2.25],
|
||||
[70.5, 'C-', 2.00],
|
||||
[69.5, 'D' , 1.00],
|
||||
[0 , 'F' , 0.00]
|
||||
];
|
||||
|
||||
// Keywords
|
||||
public static MIN = 0;
|
||||
public static LETTER = 1;
|
||||
public static GPA = 2;
|
||||
|
||||
/**
|
||||
* Calculate GPA for a list of couses
|
||||
*
|
||||
* @param coursesOriginal List of courses
|
||||
*/
|
||||
public static getGPA(coursesOriginal: Course[]): {gpa: number, accurate: boolean, max: number}
|
||||
{
|
||||
// Clone array
|
||||
let courses: Course[] = [];
|
||||
|
||||
// Accurate or not
|
||||
let accurate: boolean = true;
|
||||
|
||||
// Remove all courses that does not have a grade
|
||||
coursesOriginal.forEach(course =>
|
||||
{
|
||||
if (course.letterGrade == null || course.letterGrade == '')
|
||||
{
|
||||
accurate = false;
|
||||
}
|
||||
else if (course.level != 'none')
|
||||
{
|
||||
courses.push(course);
|
||||
}
|
||||
});
|
||||
|
||||
// If no course have grade, return -1
|
||||
if (courses.length == 0)
|
||||
{
|
||||
return {gpa: -1, accurate: false, max: -1};
|
||||
}
|
||||
|
||||
// Count total GPA
|
||||
let totalGPA = 0;
|
||||
let maxTotal = 0;
|
||||
courses.forEach(course =>
|
||||
{
|
||||
totalGPA += this.getGP(course, course.letterGrade);
|
||||
maxTotal += this.getGP(course, 'A+');
|
||||
});
|
||||
|
||||
// Get average GPA, round to two decimal places
|
||||
let gpa = Math.round(totalGPA / courses.length * 100) / 100;
|
||||
let maxGPA = Math.round(maxTotal / courses.length * 100) / 100;
|
||||
|
||||
// Return results
|
||||
return {gpa: gpa, accurate: accurate, max: maxGPA};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate GPA for a course
|
||||
*
|
||||
* @param course Course
|
||||
* @param letterGrade Letter grade
|
||||
*/
|
||||
public static getGP(course: Course, letterGrade?: string): number
|
||||
{
|
||||
// Find the GPA for this course.
|
||||
for (let scale of this.SCALE)
|
||||
{
|
||||
// Letter grades are the same
|
||||
if (scale[this.LETTER] == letterGrade)
|
||||
{
|
||||
// Get grade and add it
|
||||
let grade = <number> scale[this.GPA];
|
||||
|
||||
// Add scaleUp if not failed.
|
||||
if (grade != 0) grade += course.scaleUp;
|
||||
|
||||
// That's it
|
||||
return grade;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the total-mean (total/max) average
|
||||
*
|
||||
* @param course Course
|
||||
*/
|
||||
public static getTotalMeanAverage(course: Course)
|
||||
{
|
||||
let score = 0;
|
||||
let max = 0;
|
||||
|
||||
// Loop through assignments
|
||||
course.assignments.forEach(assignment =>
|
||||
{
|
||||
// If assignment should be displayed
|
||||
if (assignment.complete != 'Complete') return;
|
||||
|
||||
// Record scores
|
||||
score += assignment.score;
|
||||
max += assignment.scoreMax;
|
||||
});
|
||||
|
||||
// Return
|
||||
return score / max * 100;
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import {Grade} from '@/components/app/app';
|
||||
|
||||
export default class JsonUtils
|
||||
{
|
||||
/**
|
||||
* This method filters the information provided in an assignments json.
|
||||
*
|
||||
* @param assignments Assignments object
|
||||
* @returns Grade[] Filtered assignment grade object list
|
||||
*/
|
||||
public static filterAssignments(assignments: any): Grade[]
|
||||
{
|
||||
let result: Grade[] = [];
|
||||
|
||||
assignments.assignments.forEach((assignment: any) =>
|
||||
{
|
||||
result.push(
|
||||
{
|
||||
type: assignment.assignment_type,
|
||||
description: assignment.assignment_description,
|
||||
date: assignment._date,
|
||||
complete: assignment.completion_status,
|
||||
include: assignment.include_in_calculated_grade == 1,
|
||||
display: assignment.display_grade == 1,
|
||||
|
||||
scoreMax: assignment.maximum_score,
|
||||
score: +assignment.raw_score
|
||||
});
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -15,16 +15,17 @@
|
||||
"webpack-env"
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"scripthost"
|
||||
]
|
||||
],
|
||||
|
||||
// Custom
|
||||
"strictPropertyInitialization": false
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
|
||||