Compare commits
1558 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 | |||
| 35a85d8e83 | |||
| 71b0a6e4dd | |||
| d4fbd04466 | |||
| 31b3814b1e | |||
| 1aff2b0a68 | |||
| 2567fcadbd | |||
| d9e0e9f84e | |||
| 59b31ea43f | |||
| 2c8b3e0f84 | |||
| 6910a7b5ea | |||
| cbdcfc4ca1 | |||
| feabc336c1 | |||
| b5b9f14a49 | |||
| fc93cd1248 | |||
| 6e58c634a1 | |||
| 10cca344c7 | |||
| cdf58ea3a0 | |||
| d97c80afbb | |||
| 8c7d028f5b | |||
| 19ba8ec7e6 | |||
| 6937952f3a | |||
| 485717d3ef | |||
| 3f3e46bec5 | |||
| 1494315e0c | |||
| 76eb8168ce | |||
| e4a1c4973d | |||
| b22fc41582 | |||
| 0e1264d938 | |||
| a94a6db4b7 | |||
| 18b5b7de77 | |||
| 28d4e5d871 | |||
| cc5b7b2793 | |||
| 7b4ced4e94 | |||
| 70e6debe82 | |||
| e0c4426ebb | |||
| 495e776971 | |||
| fc625c8edb | |||
| 6c1623cec5 | |||
| a9a9cf5f11 | |||
| d8f276005d | |||
| 5c9e67478f | |||
| 5e0394f11c | |||
| 774e436c9c | |||
| 8b653e3317 | |||
| 9731d4e380 | |||
| e3ca21ec29 | |||
| a89c29dc53 | |||
| 87a59416ec | |||
| 8243312179 | |||
| 3dcc1b8319 | |||
| 899ff22677 | |||
| 788f51399a | |||
| 575dbdf765 | |||
| 1659964011 | |||
| 02533f6b07 | |||
| 68a91e5eb8 | |||
| 1eb6128aac | |||
| fbd59f9ec3 | |||
| 7eb7838f66 | |||
| 6459e1c09b | |||
| 4461ef835f | |||
| 6ee207d754 | |||
| 3c0c0d5f1b | |||
| bc8fcc021d | |||
| e9a6cfe5c5 | |||
| 0e92cedf54 | |||
| a73d6b0399 | |||
| 8042765ff5 | |||
| 74a033fb5d | |||
| cc4378d905 | |||
| 0fd3696342 | |||
| 3de4b1f6e6 | |||
| 610d1d190d | |||
| c262584e2f | |||
| a526946b83 | |||
| b78c5b4f5b | |||
| 8978b7ca7b | |||
| d254706a21 | |||
| 8dd7d35abe | |||
| 2b989fb137 | |||
| e2a13e90e0 | |||
| 3546a57711 | |||
| 770e26b0cf | |||
| fff60f5754 | |||
| a46e011c90 | |||
| 48224d9e34 | |||
| eb311e9f2d | |||
| 7b53e65a1b | |||
| 065d6d31a3 | |||
| d8eb160123 | |||
| 8e924d8e33 | |||
| 1e45b418b8 | |||
| e42c8be76f | |||
| b6b3f921a2 | |||
| d3234be6db | |||
| 50a5f9dcc5 | |||
| 62d40dd0a7 | |||
| 9e8e44b91f | |||
| df85e1084f | |||
| 0b41f8ac5d | |||
| 7b11537e0f | |||
| 830f55441c | |||
| 0a49d791ae | |||
| c675fc5650 | |||
| ad8496e661 | |||
| 72b9e4d214 | |||
| 419cced592 |
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 HyDEV
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,6 +1,38 @@
|
||||
# veracross-analyzer
|
||||
<h1 align="center"><br><br>
|
||||
VeracrossAnalyzer UI
|
||||
</h1>
|
||||
|
||||
## Project setup
|
||||
<h4 align="center">
|
||||
A Website, A Visual Representation of Students' Grade Data on Veracross
|
||||
</h4>
|
||||
|
||||
<h5 align="center">
|
||||
<a href="#intro">Introduction</a>
|
||||
<a href="#setup">Project Setup</a>
|
||||
<a href="#license">License</a>
|
||||
</h5><br><br><br>
|
||||
|
||||
|
||||
|
||||
<a name="intro"></a>
|
||||
Introduction:
|
||||
--------
|
||||
|
||||
This is a website that generates visual representation of students' grade data on Veracross. Currently there is only one graph and one numerical data representing the GPA. But also it just released yesterday! (Yay!) What do you expect this soon lol?
|
||||
|
||||
**Here's how it looks like right now:** *(Now all of you know my grades ;-;)*
|
||||
|
||||

|
||||
|
||||
<br>
|
||||
|
||||
<a name="setup"></a>
|
||||
Project Setup:
|
||||
--------
|
||||
|
||||
TODO: Actually write a project setup tutorial that's not generated by Vue on initialization ;-;.
|
||||
|
||||
### Install
|
||||
```
|
||||
npm install
|
||||
```
|
||||
@@ -15,15 +47,10 @@ npm run serve
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Run your tests
|
||||
```
|
||||
npm run test
|
||||
```
|
||||
<br>
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
npm run lint
|
||||
```
|
||||
<a name="license"></a>
|
||||
License: [MIT](https://choosealicense.com/licenses/mit/)
|
||||
--------
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||
The MIT license basically means that this project is open-soucred and you can do whatever you want with it, as long as you include a copy of this license in your distribution. You don't have to ask for permissions to use or anything. However, if you do bad things with it, I'm not responsible.
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
echo "Switch to production database settings"
|
||||
exit
|
||||
|
||||
# abort on errors
|
||||
set -e
|
||||
|
||||
# build
|
||||
npm run build
|
||||
|
||||
# navigate into the build output directory
|
||||
cd dist
|
||||
|
||||
# if you are deploying to a custom domain
|
||||
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: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,195 +1,258 @@
|
||||
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';
|
||||
|
||||
/**
|
||||
* 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,
|
||||
|
||||
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[] = [];
|
||||
|
||||
// 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
|
||||
*
|
||||
* @param courses Courses Json
|
||||
*/
|
||||
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(() =>
|
||||
{
|
||||
// When the assignments are ready
|
||||
this.assignmentsReady = true;
|
||||
// Filter courses
|
||||
this.gradedCourses = this.courses.filter(c => c.isGraded);
|
||||
|
||||
// Check grading algorithms
|
||||
this.checkGradingAlgorithms();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
// Show loading message
|
||||
this.logLoading('2. Checking grading algorithms...');
|
||||
|
||||
// Loop through all the courses
|
||||
for (const course of this.gradedCourses)
|
||||
{
|
||||
if (course.assignments == null) return false;
|
||||
for (const i of [0, 1, 2, 3])
|
||||
{
|
||||
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 (resp.success)
|
||||
{
|
||||
// Add it to course
|
||||
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(e => this.showError(`Error: Grading data failed to load.\n(${e})`))
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
// Wait for done
|
||||
pWaitFor(() => this.gradedCourses.every(c => c.termGrading.every(g => g != null))).then(() =>
|
||||
{
|
||||
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()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
showUpdates()
|
||||
{
|
||||
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));
|
||||
@@ -197,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,16 +1,34 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<login v-if="showLogin" v-on:login:token="onLogin" :http="http"></login>
|
||||
<navigation :courses="courses"
|
||||
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="courses" 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,73 +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.
|
||||
*/
|
||||
@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;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cookies
|
||||
*/
|
||||
clearCookies()
|
||||
{
|
||||
this.$cookies.keys().forEach(key => this.$cookies.remove(key));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,28 +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 !== ''}">
|
||||
</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 !== ''}">
|
||||
</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,18 +1,81 @@
|
||||
/**
|
||||
* This class stores the static constants.
|
||||
*/
|
||||
import {findLastIndex} from '@/logic/utils/general-utils';
|
||||
|
||||
export default class Constants
|
||||
{
|
||||
/**
|
||||
* Base url for api access
|
||||
* TODO: Use https for actual usage
|
||||
*/
|
||||
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.1.1.0 `---\' '
|
||||
' `---\' \n' +
|
||||
` Version v${Constants.VERSION} by Hykilpikonna (YGui21)\n` +
|
||||
` Github: ${Constants.GITHUB}`;
|
||||
|
||||
// Graph Theme
|
||||
static THEME =
|
||||
{
|
||||
// Colors
|
||||
colors:
|
||||
[
|
||||
'#19d4ae',
|
||||
'#5ab1ef',
|
||||
'#fa6e86',
|
||||
'#ffb980',
|
||||
'#0067a6',
|
||||
'#c4b4e4',
|
||||
'#d87a80',
|
||||
'#9cbbff',
|
||||
'#d9d0c7',
|
||||
'#87a997',
|
||||
'#d49ea2',
|
||||
'#5b4947',
|
||||
'#7ba3a8',
|
||||
'#fc97af',
|
||||
'#919e8b',
|
||||
'#d7ab82',
|
||||
'#6e7074',
|
||||
'#61a0a8',
|
||||
'#efa18d',
|
||||
'#787464',
|
||||
'#cc7e63',
|
||||
'#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,21 +0,0 @@
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import {Course} from '@/components/app/app';
|
||||
|
||||
@Component({
|
||||
})
|
||||
export default class GraphOverall extends Vue
|
||||
{
|
||||
@Prop({required: true}) chart: any;
|
||||
|
||||
private settings =
|
||||
{
|
||||
series:
|
||||
{
|
||||
smooth: false
|
||||
},
|
||||
yAxis:
|
||||
{
|
||||
min: 70
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<template>
|
||||
<div id="graph-overall">
|
||||
<p>Your average score graph all time:</p>
|
||||
<ve-line :data="chart" :extend="settings"></ve-line>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./graph-overall.ts" lang="ts"></script>
|
||||
<style src="./graph-overall.scss" lang="scss"></style>
|
||||
@@ -0,0 +1,116 @@
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
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 OverallBar extends Vue
|
||||
{
|
||||
@Prop({required: true}) courses: Course[];
|
||||
|
||||
/**
|
||||
* Generate settings
|
||||
*/
|
||||
get chartSettings()
|
||||
{
|
||||
let settings =
|
||||
{
|
||||
// Title
|
||||
title:
|
||||
{
|
||||
show: true,
|
||||
textStyle:
|
||||
{
|
||||
fontSize: 12
|
||||
},
|
||||
text: 'Course GPA',
|
||||
subtext: 'Current GPA for every course',
|
||||
x: 'center'
|
||||
},
|
||||
|
||||
// X axis represents course names
|
||||
xAxis:
|
||||
{
|
||||
type: 'category',
|
||||
axisLabel: {
|
||||
interval: 0,
|
||||
inside: false,
|
||||
rotate: 90,
|
||||
|
||||
// Truncate text length
|
||||
formatter: (value: string) => FormatUtils.limit(value, 16)
|
||||
},
|
||||
},
|
||||
|
||||
// Y axis represents GPAs and MaxGPAs
|
||||
yAxis:
|
||||
{
|
||||
type: 'value'
|
||||
},
|
||||
|
||||
// Data
|
||||
series:
|
||||
[
|
||||
// Max GP
|
||||
{
|
||||
type: 'bar',
|
||||
barGap: '-100%',
|
||||
data: this.courses.map(course =>
|
||||
{
|
||||
return {value: [course.name, GPAUtils.getGP(course, 'A+')], itemStyle: {color: '#d8d8d8'}}
|
||||
}),
|
||||
},
|
||||
// Current GP
|
||||
{
|
||||
type: 'bar',
|
||||
barGap: '-100%',
|
||||
data: this.generateGPData(),
|
||||
|
||||
label:
|
||||
{
|
||||
show: true,
|
||||
rotate: 90
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
// Disable tooltip
|
||||
tooltip:
|
||||
{
|
||||
show: false
|
||||
}
|
||||
};
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate GP data for each course
|
||||
*/
|
||||
private generateGPData()
|
||||
{
|
||||
let data: any = [];
|
||||
|
||||
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, gp],
|
||||
itemStyle:
|
||||
{
|
||||
color: Constants.THEME.colors[index]
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -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,29 +1,60 @@
|
||||
|
||||
// Add some margins
|
||||
.el-card
|
||||
.gpa-card
|
||||
{
|
||||
margin: 10px;
|
||||
height: 494px;
|
||||
padding: 0;
|
||||
|
||||
// Vertical center
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
margin-left: 20px;
|
||||
min-width: 136px;
|
||||
}
|
||||
|
||||
.span-gpa-header
|
||||
.gpa
|
||||
{
|
||||
display: block;
|
||||
}
|
||||
|
||||
.gpa.header
|
||||
{
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.span-gpa
|
||||
.gpa.text
|
||||
{
|
||||
font-size: 35px;
|
||||
font-family: 'Avenir', Helvetica, Arial, sans-serif;
|
||||
font-family: var(--font);
|
||||
}
|
||||
|
||||
.gpa-time
|
||||
.gpa.max
|
||||
{
|
||||
font-size: 14px;
|
||||
margin-top: -10px;
|
||||
margin-bottom: 10px;
|
||||
font-size: 12px;
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.gpa.time
|
||||
{
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.no-grade
|
||||
{
|
||||
font-size: 30px;
|
||||
color: #b1b1b1;
|
||||
|
||||
// Disable selecting
|
||||
display:block;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
// Cards
|
||||
.el-card.overall-bar-card
|
||||
{
|
||||
margin-right: 20px;
|
||||
min-width: 170px;
|
||||
}
|
||||
|
||||
.dialog-checkbox
|
||||
{
|
||||
display: block;
|
||||
margin-top: 20px;
|
||||
margin-bottom: -20px;
|
||||
}
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import GraphOverall from '@/pages/overall/graph-overall/graph-overall';
|
||||
import {Course} from '@/components/app/app';
|
||||
import {GPAUtils} from '@/utils/gpa-utils';
|
||||
|
||||
@Component({
|
||||
components: {GraphOverall}
|
||||
})
|
||||
export default class Overall extends Vue
|
||||
{
|
||||
// @ts-ignore
|
||||
@Prop({required: true}) courses: Course[];
|
||||
|
||||
get convertCharts()
|
||||
{
|
||||
// Null case
|
||||
if (this.courses == null) return [];
|
||||
|
||||
// Filter it
|
||||
let courses: Course[] = this.filterCourses();
|
||||
|
||||
// Compute the column names
|
||||
let columns = ['date'];
|
||||
courses.forEach(course =>
|
||||
{
|
||||
columns.push(course.name);
|
||||
});
|
||||
|
||||
// Find the min date
|
||||
let minDate: Date = new Date();
|
||||
courses.forEach(course =>
|
||||
{
|
||||
if (course.assignments.length == 0) return;
|
||||
let date = new Date(course.assignments[course.assignments.length - 1].date);
|
||||
if (date < minDate) minDate = date;
|
||||
});
|
||||
|
||||
// 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));
|
||||
}
|
||||
|
||||
// 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())
|
||||
{
|
||||
// Record scores
|
||||
courseScores[course.name] += assignment.score;
|
||||
courseMaxScores[course.name] += assignment.scoreMax;
|
||||
}
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
console.log(rows);
|
||||
|
||||
return {
|
||||
columns: columns,
|
||||
rows: rows
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of courses that are graphed
|
||||
*/
|
||||
private filterCourses(): Course[]
|
||||
{
|
||||
// Define result
|
||||
let result: Course[] = [];
|
||||
|
||||
// Filter through courses
|
||||
this.courses.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;
|
||||
|
||||
// Add it to the list
|
||||
result.push(course);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is called to get gpa as a string.
|
||||
*/
|
||||
public getGPA()
|
||||
{
|
||||
let gpa = GPAUtils.getGPA(this.courses);
|
||||
let result = '' + gpa.gpa;
|
||||
|
||||
/* Not accurate
|
||||
if (!gpa.accurate)
|
||||
{
|
||||
result = `(${result})`;
|
||||
}*/
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,141 @@
|
||||
<template>
|
||||
<div id="overall">
|
||||
<el-row>
|
||||
<el-col :span="4">
|
||||
<el-card style="margin-left: 20px">
|
||||
<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="span-gpa-header">GPA:</span>
|
||||
<br>
|
||||
<span class="span-gpa">{{getGPA()}}</span>
|
||||
<div class="bottom clearfix gpa-time">
|
||||
<time class="time">{{ new Date().toDateString() }}</time>
|
||||
<br>
|
||||
<el-button type="text" class="button">Button</el-button>
|
||||
<span class="gpa header">GPA:</span>
|
||||
<span class="gpa text">{{getGPA().gpa}}</span>
|
||||
<span class="gpa max">(Out of {{getGPA().max}})</span>
|
||||
<div class="bottom clearfix gpa time">
|
||||
<time>{{ new Date().toDateString() }}</time>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="20">
|
||||
<el-card style="margin-right: 20px">
|
||||
<graph-overall :chart="convertCharts"></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" 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,101 +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}
|
||||
{
|
||||
// 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.numericGrade == null || course.numericGrade == 0)
|
||||
{
|
||||
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};
|
||||
}
|
||||
|
||||
// Count total GPA
|
||||
let totalGPA = 0;
|
||||
courses.forEach(course =>
|
||||
{
|
||||
totalGPA += this.getGP(course);
|
||||
});
|
||||
|
||||
// Get average GPA, round to two decimal places
|
||||
let gpa = Math.round(totalGPA / courses.length * 100) / 100;
|
||||
|
||||
// Return results
|
||||
return {gpa: gpa, accurate: accurate};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate GPA for a course
|
||||
*
|
||||
* @param course Course
|
||||
*/
|
||||
public static getGP(course: Course): number
|
||||
{
|
||||
// Find the GPA for this course.
|
||||
for (let scale of this.SCALE)
|
||||
{
|
||||
// Letter grades are the same
|
||||
if (scale[this.LETTER] == course.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;
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
"defaultSeverity": "warning",
|
||||
"linterOptions": {
|
||||
"exclude": [
|
||||
"node_modules/**"
|
||||
"node_modules/**",
|
||||
"*.json",
|
||||
"**/*.json"
|
||||
]
|
||||
},
|
||||
"rules": {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
module.exports = {
|
||||
devServer: {
|
||||
module.exports =
|
||||
{
|
||||
devServer:
|
||||
{
|
||||
disableHostCheck: true,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||