Compare commits
675 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -22,7 +22,7 @@ This is a website that generates visual representation of students' grade data o
|
|||||||
|
|
||||||
**Here's how it looks like right now:** *(Now all of you know my grades ;-;)*
|
**Here's how it looks like right now:** *(Now all of you know my grades ;-;)*
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
|
|||||||
Generated
+5
@@ -6935,6 +6935,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"moment": {
|
||||||
|
"version": "2.24.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
|
||||||
|
"integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg=="
|
||||||
|
},
|
||||||
"move-concurrently": {
|
"move-concurrently": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npm.taobao.org/move-concurrently/download/move-concurrently-1.0.1.tgz",
|
"resolved": "https://registry.npm.taobao.org/move-concurrently/download/move-concurrently-1.0.1.tgz",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"core-js": "^2.6.5",
|
"core-js": "^2.6.5",
|
||||||
"echarts": "^4.2.1",
|
"echarts": "^4.2.1",
|
||||||
"element-ui": "^2.11.1",
|
"element-ui": "^2.11.1",
|
||||||
|
"moment": "^2.24.0",
|
||||||
"p-wait-for": "^3.1.0",
|
"p-wait-for": "^3.1.0",
|
||||||
"v-charts": "^1.19.0",
|
"v-charts": "^1.19.0",
|
||||||
"vue": "^2.6.10",
|
"vue": "^2.6.10",
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -25,4 +25,14 @@
|
|||||||
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
|
<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>
|
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
|
||||||
</body>
|
</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>
|
</html>
|
||||||
|
|||||||
@@ -1,8 +1,85 @@
|
|||||||
#app
|
#app
|
||||||
{
|
{
|
||||||
font-family: 'Avenir', Helvetica, Arial, sans-serif;
|
font-family: var(--font);
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #2c3e50;
|
color: #2c3e50;
|
||||||
|
margin-bottom: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#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;
|
||||||
|
|
||||||
|
--font: 'Avenir', Helvetica, Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ##############
|
||||||
|
// # 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;
|
||||||
}
|
}
|
||||||
|
|||||||
+126
-69
@@ -1,50 +1,41 @@
|
|||||||
import {Component, Vue} from 'vue-property-decorator';
|
import {Component, Vue} from 'vue-property-decorator';
|
||||||
import Login from '@/components/login/login';
|
import Login from '@/components/login/login';
|
||||||
import Navigation from '@/components/navigation/navigation';
|
import Navigation from '@/components/navigation/navigation';
|
||||||
import Overall from '@/pages/overall/overall';
|
import Overall from '@/pages/overall/overall.vue';
|
||||||
import Constants from '@/constants';
|
import Constants from '@/constants';
|
||||||
import JsonUtils from '@/utils/json-utils';
|
|
||||||
import pWaitFor from 'p-wait-for';
|
import pWaitFor from 'p-wait-for';
|
||||||
import {HttpUtils} from '@/utils/http-utils';
|
import {HttpUtils} from '@/utils/http-utils';
|
||||||
|
import {GPAUtils} from '@/utils/gpa-utils';
|
||||||
|
import Loading from '@/components/loading/loading.vue';
|
||||||
|
import CoursePage from '@/pages/course/course-page.vue';
|
||||||
|
import Course from '@/logic/course';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Objects of this interface represent assignment grades.
|
* Objects of this interface represent assignment grades.
|
||||||
*/
|
*/
|
||||||
export interface Grade
|
export interface Assignment
|
||||||
{
|
{
|
||||||
type: string,
|
id: number
|
||||||
description: string,
|
scoreId: number
|
||||||
date: string,
|
type: string
|
||||||
complete: string,
|
typeId: number
|
||||||
include: boolean,
|
description: string
|
||||||
display: boolean,
|
date: Date
|
||||||
|
complete: string
|
||||||
|
include: boolean
|
||||||
|
display: boolean
|
||||||
|
|
||||||
scoreMax: number,
|
unread: boolean
|
||||||
|
|
||||||
|
scoreMax: number
|
||||||
score: number
|
score: number
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
gradingPeriod: 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[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {Login, Navigation, Overall},
|
components: {Login, Navigation, Overall, Loading, CoursePage},
|
||||||
})
|
})
|
||||||
export default class App extends Vue
|
export default class App extends Vue
|
||||||
{
|
{
|
||||||
@@ -54,6 +45,9 @@ export default class App extends Vue
|
|||||||
// List of course that the student takes
|
// List of course that the student takes
|
||||||
public courses: Course[] = [];
|
public courses: Course[] = [];
|
||||||
|
|
||||||
|
// List of course that should be displayed
|
||||||
|
public filteredCourses: Course[] = [];
|
||||||
|
|
||||||
// Currently selected tab
|
// Currently selected tab
|
||||||
public selectedTab: string = 'overall';
|
public selectedTab: string = 'overall';
|
||||||
|
|
||||||
@@ -63,8 +57,17 @@ export default class App extends Vue
|
|||||||
// Token
|
// Token
|
||||||
public token: string = '';
|
public token: string = '';
|
||||||
|
|
||||||
|
// Loading text
|
||||||
|
public loading: string = '';
|
||||||
|
|
||||||
|
// Loading error
|
||||||
|
public loadingError: boolean = false;
|
||||||
|
|
||||||
// Http Client
|
// Http Client
|
||||||
public http: HttpUtils = new HttpUtils('');
|
public static http: HttpUtils = new HttpUtils();
|
||||||
|
|
||||||
|
// Instance
|
||||||
|
public static instance: App;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is called when the instance is created.
|
* This is called when the instance is created.
|
||||||
@@ -73,6 +76,9 @@ export default class App extends Vue
|
|||||||
{
|
{
|
||||||
// Show splash
|
// Show splash
|
||||||
console.log(Constants.SPLASH);
|
console.log(Constants.SPLASH);
|
||||||
|
|
||||||
|
// Update instance
|
||||||
|
App.instance = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -85,11 +91,14 @@ export default class App extends Vue
|
|||||||
// Hide login bar
|
// Hide login bar
|
||||||
this.showLogin = false;
|
this.showLogin = false;
|
||||||
|
|
||||||
|
// Show loading message
|
||||||
|
this.logLoading('1. Logging in...');
|
||||||
|
|
||||||
// Store token
|
// Store token
|
||||||
this.token = token;
|
this.token = token;
|
||||||
|
|
||||||
// Assign token to http client
|
// Assign token to http client
|
||||||
this.http.token = token;
|
App.http.token = token;
|
||||||
|
|
||||||
// Load data
|
// Load data
|
||||||
this.loadCoursesAfterLogin();
|
this.loadCoursesAfterLogin();
|
||||||
@@ -100,90 +109,127 @@ export default class App extends Vue
|
|||||||
*/
|
*/
|
||||||
public loadCoursesAfterLogin()
|
public loadCoursesAfterLogin()
|
||||||
{
|
{
|
||||||
this.http.post('/courses', {}).then(response =>
|
// Show loading message
|
||||||
|
this.logLoading('2. Loading courses...');
|
||||||
|
|
||||||
|
// Post request
|
||||||
|
App.http.post('/courses', {}).then(response =>
|
||||||
{
|
{
|
||||||
// Check success
|
// Check success
|
||||||
if (response.success)
|
if (response.success)
|
||||||
{
|
{
|
||||||
// Save courses
|
// Save courses
|
||||||
this.courses = response.data;
|
this.courses = response.data.map((courseJson: any) => new Course(courseJson));
|
||||||
|
|
||||||
// Load assignments
|
// Load assignments
|
||||||
this.loadAssignments();
|
this.loadAssignments();
|
||||||
}
|
}
|
||||||
else
|
else throw new Error(response.data);
|
||||||
{
|
|
||||||
// Show error message TODO: Show it properly
|
|
||||||
alert(response.data);
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(alert);
|
.catch(e => this.showError(`Error: Course data failed to load.\n(${e})`));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load the assignments of the courses
|
* Load the assignments of the courses
|
||||||
*
|
|
||||||
* @param courses Courses Json
|
|
||||||
*/
|
*/
|
||||||
public loadAssignments()
|
public loadAssignments()
|
||||||
{
|
{
|
||||||
|
// Show loading message
|
||||||
|
this.logLoading('3. Loading assignments...');
|
||||||
|
|
||||||
// Get assignments for all the courses
|
// Get assignments for all the courses
|
||||||
this.courses.forEach(course =>
|
this.courses.forEach(course =>
|
||||||
{
|
{
|
||||||
// Send request to get assignments
|
// 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
|
// Check success
|
||||||
if (response.success)
|
if (response.success)
|
||||||
{
|
{
|
||||||
// Load assignments
|
course.loadAssignments(response.data);
|
||||||
// Parse json and filter it
|
|
||||||
course.assignments = JsonUtils.filterAssignments(response.data);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Show error message TODO: Show it properly
|
|
||||||
alert(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.
|
// Wait for assignments to be ready.
|
||||||
pWaitFor(() => this.isAssignmentsReady()).then(() =>
|
pWaitFor(() => this.courses.every(c => c.rawAssignments != null)).then(() =>
|
||||||
{
|
{
|
||||||
// When the assignments are ready
|
// Filter courses
|
||||||
this.assignmentsReady = true;
|
this.filteredCourses = this.courses.filter(c => c.isGraded);
|
||||||
|
|
||||||
|
// Check grading algorithms
|
||||||
|
this.checkGradingAlgorithms();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Are assignments ready or not
|
* Check the courses' grading algorithms. (Total-mean or percent-type)
|
||||||
*
|
|
||||||
* @returns boolean Ready or not
|
|
||||||
*/
|
*/
|
||||||
private isAssignmentsReady(): boolean
|
private checkGradingAlgorithms()
|
||||||
{
|
{
|
||||||
for (const course of this.courses)
|
// Show loading message
|
||||||
|
this.logLoading('4. Checking grading algorithms...');
|
||||||
|
|
||||||
|
// Loop through all the courses
|
||||||
|
for (const course of this.filteredCourses)
|
||||||
{
|
{
|
||||||
if (course.assignments == null) return false;
|
let termGrade = +GPAUtils.getTotalMeanAverage(course.computed.termAssignments[Constants.CURRENT_TERM]).toFixed(2);
|
||||||
|
|
||||||
|
// Check if total-average grade is the same with percent-type grade
|
||||||
|
if (course.rawNumericGrade == termGrade)
|
||||||
|
{
|
||||||
|
course.grading = {method: 'TOTAL_MEAN', weightingMap: {}};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Request grading scheme for this course
|
||||||
|
App.http.post('/grading', {'assignmentsId': course.assignmentsId}).then(response =>
|
||||||
|
{
|
||||||
|
// Check success
|
||||||
|
if (response.success)
|
||||||
|
{
|
||||||
|
// Add it to course
|
||||||
|
course.grading = response.data;
|
||||||
|
}
|
||||||
|
else throw new Error(response.data);
|
||||||
|
})
|
||||||
|
.catch(e => this.showError(`Error: Grading data failed to load.\n(${e})`))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
// Wait for done
|
||||||
|
pWaitFor(() => this.filteredCourses.every(c => c.grading != undefined)).then(() =>
|
||||||
|
{
|
||||||
|
// When the assignments are ready
|
||||||
|
this.assignmentsReady = true;
|
||||||
|
|
||||||
|
// Remove loading
|
||||||
|
this.logLoading('');
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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)
|
private logLoading(message: string)
|
||||||
{
|
{
|
||||||
// Debug output TODO: Remove this
|
if (message == '') this.loading = '';
|
||||||
console.log(tab);
|
else this.loading += '\n' + message;
|
||||||
|
}
|
||||||
|
|
||||||
// Update selected tab
|
/**
|
||||||
this.selectedTab = tab;
|
* Show error message on loading screen
|
||||||
|
*
|
||||||
|
* @param message Error message
|
||||||
|
*/
|
||||||
|
private showError(message: string)
|
||||||
|
{
|
||||||
|
this.loadingError = true;
|
||||||
|
this.loading = message;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -197,4 +243,15 @@ export default class App extends Vue
|
|||||||
// Refresh
|
// Refresh
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select time (Eg. Term 1, Term 2, All Year, etc.)
|
||||||
|
*
|
||||||
|
* @param code
|
||||||
|
*/
|
||||||
|
public selectTime(code: number)
|
||||||
|
{
|
||||||
|
// TODO: Optimize
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div id="app" class="theme-default">
|
||||||
<login v-if="showLogin" v-on:login:token="onLogin" :http="http"></login>
|
<login v-if="showLogin" v-on:login:token="onLogin"></login>
|
||||||
<navigation :courses="courses"
|
<navigation :courses="filteredCourses"
|
||||||
v-on:sign-out="signOut()"
|
:activeIndex.sync="selectedTab"
|
||||||
v-on:navigation:select="onNavigate">
|
@sign-out="signOut" @select-time="selectTime">
|
||||||
</navigation>
|
</navigation>
|
||||||
|
|
||||||
<div id="app-content">
|
<div id="app-content" v-if="assignmentsReady && loading === ''">
|
||||||
<overall :courses="courses" v-if="selectedTab === 'overall' && assignmentsReady"></overall>
|
<overall v-if="selectedTab === 'overall'"
|
||||||
|
:courses="filteredCourses">
|
||||||
|
</overall>
|
||||||
|
<course-page v-if="selectedTab.split('/')[0] === 'course'"
|
||||||
|
:course="filteredCourses.find(c => +c.id === +selectedTab.split('/')[1])">
|
||||||
|
</course-page>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<loading v-if="loading !== ''" :text="loading" :error="loadingError"></loading>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
<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 getText()" :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 getText()" :style="`font-size: ${16 - getText().length + index}px;`">
|
||||||
|
{{line}}
|
||||||
|
<br>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: {}
|
||||||
|
})
|
||||||
|
export default class Loading extends Vue
|
||||||
|
{
|
||||||
|
@Prop({required: true}) text: string;
|
||||||
|
|
||||||
|
@Prop({required: true}) error: boolean;
|
||||||
|
|
||||||
|
getText()
|
||||||
|
{
|
||||||
|
return this.text.split('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
import {Component, Vue} from 'vue-property-decorator';
|
||||||
import Constants from '@/constants';
|
import Constants from '@/constants';
|
||||||
import {HttpUtils} from '@/utils/http-utils';
|
import App from '@/components/app/app';
|
||||||
|
import VersionUtils from '@/utils/version-utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component handles user login, and obtains data from the server.
|
* This component handles user login, and obtains data from the server.
|
||||||
* TODO: Press enter to login
|
|
||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
components: {},
|
components: {},
|
||||||
@@ -17,9 +17,6 @@ export default class Login extends Vue
|
|||||||
public loading: boolean = false;
|
public loading: boolean = false;
|
||||||
public error: String = '';
|
public error: String = '';
|
||||||
|
|
||||||
@Prop()
|
|
||||||
public http?: HttpUtils;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is called when the instance is created.
|
* This is called when the instance is created.
|
||||||
*/
|
*/
|
||||||
@@ -28,9 +25,39 @@ export default class Login extends Vue
|
|||||||
// Check login cookies
|
// Check login cookies
|
||||||
if (this.$cookies.isKey('va.token'))
|
if (this.$cookies.isKey('va.token'))
|
||||||
{
|
{
|
||||||
// Already contains valid token / TODO: Validate
|
// Check cookies version
|
||||||
this.$emit('login:token', this.$cookies.get('va.token'));
|
if (this.needToUpdateCookies())
|
||||||
|
{
|
||||||
|
console.log('Version Updated! Clearing cookies...');
|
||||||
|
|
||||||
|
// Clear all cookies
|
||||||
|
this.$cookies.keys().forEach(key => this.$cookies.remove(key));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Already contains valid token / TODO: Validate
|
||||||
|
// TODO: Update token each access
|
||||||
|
this.$emit('login:token', this.$cookies.get('va.token'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
console.log('Cookies doesn\'t exist');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check version number
|
||||||
|
*
|
||||||
|
* @returns boolean Need to clear cookies or not
|
||||||
|
*/
|
||||||
|
public needToUpdateCookies(): boolean
|
||||||
|
{
|
||||||
|
// Version doesn't exist
|
||||||
|
if (!this.$cookies.isKey('va.version')) 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -42,14 +69,15 @@ export default class Login extends Vue
|
|||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
|
||||||
// Fetch request
|
// Fetch request
|
||||||
(<HttpUtils> this.http).post('/login', {username: this.username, password: this.password})
|
App.http.post('/login', {username: this.username, password: this.password})
|
||||||
.then(response =>
|
.then(response =>
|
||||||
{
|
{
|
||||||
// Check success
|
// Check success
|
||||||
if (response.success)
|
if (response.success)
|
||||||
{
|
{
|
||||||
// Save token to cookies
|
// Save token to cookies
|
||||||
this.$cookies.set('va.token', response.data, '7d');
|
this.$cookies.set('va.token', response.data, '27d');
|
||||||
|
this.$cookies.set('va.version', Constants.VERSION, '27d');
|
||||||
|
|
||||||
// Call custom event with token
|
// Call custom event with token
|
||||||
this.$emit('login:token', response.data);
|
this.$emit('login:token', response.data);
|
||||||
|
|||||||
@@ -5,23 +5,24 @@
|
|||||||
<img alt="Vue logo" src="../../assets/logo.png">
|
<img alt="Vue logo" src="../../assets/logo.png">
|
||||||
|
|
||||||
<h1>Veracross Analyzer</h1>
|
<h1>Veracross Analyzer</h1>
|
||||||
|
<form id="login-form">
|
||||||
|
<el-input v-model="username"
|
||||||
|
placeholder="School Username"
|
||||||
|
:class="{'input-error': error !== ''}"
|
||||||
|
@keyup.enter.native="onEnter">
|
||||||
|
</el-input>
|
||||||
|
|
||||||
<el-input v-model="username"
|
<el-input v-model="password"
|
||||||
placeholder="School Username"
|
placeholder="Veracross Password"
|
||||||
:class="{'input-error': error !== ''}"
|
show-password=""
|
||||||
@keyup.enter.native="onEnter">
|
:class="{'input-error': error !== ''}"
|
||||||
</el-input>
|
@keyup.enter.native="onEnter">
|
||||||
|
</el-input>
|
||||||
|
|
||||||
<el-input v-model="password"
|
<div class="el-form-item__error custom">{{error}}</div>
|
||||||
placeholder="Veracross Password"
|
|
||||||
show-password=""
|
|
||||||
:class="{'input-error': error !== ''}"
|
|
||||||
@keyup.enter.native="onEnter">
|
|
||||||
</el-input>
|
|
||||||
|
|
||||||
<div class="el-form-item__error custom">{{error}}</div>
|
<el-button plain type="primary" @click="onLoginClick" :loading="loading">Login</el-button>
|
||||||
|
</form>
|
||||||
<el-button plain type="primary" @click="onLoginClick" :loading="loading">Login</el-button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,3 +15,99 @@
|
|||||||
border-bottom-width: 0;
|
border-bottom-width: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#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: 110px;
|
||||||
|
|
||||||
|
// Margins
|
||||||
|
margin-top: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav-title
|
||||||
|
{
|
||||||
|
// Float left
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
// Set height
|
||||||
|
height: 60px;
|
||||||
|
|
||||||
|
// Center text
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
// Margins
|
||||||
|
margin-left: 20px;
|
||||||
|
margin-right: 8px;
|
||||||
|
|
||||||
|
// Font
|
||||||
|
font: bold 16px arial;
|
||||||
|
display: inline-flex;
|
||||||
|
|
||||||
|
// Shadow effect
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-color: #b1b1b1;
|
||||||
|
color: transparent;
|
||||||
|
text-shadow: 1px 1px 0 rgba(255, 255, 255, 0.85);
|
||||||
|
|
||||||
|
// Make it non-clickable
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#next-course
|
||||||
|
{
|
||||||
|
// Down center
|
||||||
|
width: 50%;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 25%;
|
||||||
|
padding-top: 2px;
|
||||||
|
box-shadow: 0 -2px 9px 0 #ecebeb;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer
|
||||||
|
{
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#prev-course
|
||||||
|
{
|
||||||
|
// Up center
|
||||||
|
width: 50%;
|
||||||
|
position: absolute;
|
||||||
|
top: 61px;
|
||||||
|
left: 25%;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
box-shadow: 0 2px 9px 0 #ecebeb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-course-operations
|
||||||
|
{
|
||||||
|
// Background
|
||||||
|
background-color: rgba(214, 214, 214, 0.67);
|
||||||
|
opacity: 0.85;
|
||||||
|
|
||||||
|
// Font
|
||||||
|
font-size: 14px;
|
||||||
|
color: #ab8585;
|
||||||
|
|
||||||
|
// Cursor
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||||
import Constants from '@/constants';
|
import App from '@/components/app/app';
|
||||||
|
import {CourseUtils} from '@/utils/course-utils';
|
||||||
|
import {FormatUtils} from '@/utils/format-utils';
|
||||||
|
import pWaitFor from 'p-wait-for';
|
||||||
|
import Course from '@/logic/course';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component is the top navigation bar
|
* This component is the top navigation bar
|
||||||
@@ -9,9 +13,63 @@ import Constants from '@/constants';
|
|||||||
})
|
})
|
||||||
export default class Navigation extends Vue
|
export default class Navigation extends Vue
|
||||||
{
|
{
|
||||||
public activeIndex: string = 'overall';
|
@Prop({required: true}) activeIndex: string;
|
||||||
|
|
||||||
@Prop() courses: any;
|
@Prop({required: true}) courses: Course[];
|
||||||
|
|
||||||
|
private gradingPeriod: string = 'All Year';
|
||||||
|
|
||||||
|
// Instance
|
||||||
|
public static instance: Navigation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is called when the instance is created.
|
||||||
|
*/
|
||||||
|
public 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.
|
||||||
|
*/
|
||||||
|
public mounted()
|
||||||
|
{
|
||||||
|
// Set instance
|
||||||
|
Navigation.instance = this;
|
||||||
|
|
||||||
|
// Set history state
|
||||||
|
let url = '/' + window.location.hash;
|
||||||
|
if (url == '/' || url == '') url = '/#overall';
|
||||||
|
window.history.replaceState({lastTab: url.substring(1)}, '', url);
|
||||||
|
|
||||||
|
// Update initial index after loading is done
|
||||||
|
pWaitFor(() => this.courses.length > 1 && App.instance.loading != '').then(() =>
|
||||||
|
{
|
||||||
|
this.updateIndex(url.substring(2), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create history state listener
|
||||||
|
window.onpopstate = e =>
|
||||||
|
{
|
||||||
|
if (e.state)
|
||||||
|
{
|
||||||
|
// Restore previous tab
|
||||||
|
console.log(`onPopState: Current: ${this.activeIndex}, Previous: ${e.state.lastTab}`);
|
||||||
|
this.updateIndex(e.state.lastTab, false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public formatCourseIndex(course: Course)
|
||||||
|
{
|
||||||
|
return CourseUtils.formatTabIndex(course);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function is called when the selection changes.
|
* This function is called when the selection changes.
|
||||||
@@ -22,18 +80,107 @@ export default class Navigation extends Vue
|
|||||||
public onSelect(index: string, indexPath: string)
|
public onSelect(index: string, indexPath: string)
|
||||||
{
|
{
|
||||||
// Update active index
|
// Update active index
|
||||||
this.activeIndex = index;
|
this.updateIndex(index);
|
||||||
|
|
||||||
// Call custom event
|
|
||||||
this.$emit('navigation:select', this.activeIndex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function is called when the sign out button is clicked.
|
* Update index
|
||||||
|
*
|
||||||
|
* @param newIndex New index
|
||||||
|
* @param history Record in history or not (Default true)
|
||||||
*/
|
*/
|
||||||
public signOut()
|
public updateIndex(newIndex: string, history?: boolean)
|
||||||
{
|
{
|
||||||
// Call custom event
|
// Call custom event
|
||||||
this.$emit('sign-out');
|
this.$emit('update:activeIndex', newIndex);
|
||||||
|
|
||||||
|
// Record or not
|
||||||
|
if (history == null || history)
|
||||||
|
{
|
||||||
|
// Check url
|
||||||
|
let url = `/#${newIndex}`;
|
||||||
|
|
||||||
|
// Push history state
|
||||||
|
window.history.pushState({lastTab: newIndex}, '', url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update title
|
||||||
|
document.title = 'Veracross Analyzer - ' + this.getTitle(newIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get title for index
|
||||||
|
*
|
||||||
|
* @param index Index
|
||||||
|
*/
|
||||||
|
public getTitle(index: string)
|
||||||
|
{
|
||||||
|
// Course
|
||||||
|
if (index.startsWith('course'))
|
||||||
|
{
|
||||||
|
return this.findCourse(index.split('/')[1], 0).name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Others
|
||||||
|
return FormatUtils.toTitleCase(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move to the next course
|
||||||
|
*
|
||||||
|
* @param indexOffset Index offset (Eg. 1 for next)
|
||||||
|
*/
|
||||||
|
public nextCourse(indexOffset: number)
|
||||||
|
{
|
||||||
|
// Set tab to the next index
|
||||||
|
this.updateIndex(CourseUtils.formatTabIndex(this.findNextCourse(indexOffset)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the next course
|
||||||
|
*
|
||||||
|
* @param indexOffset Index offset (Eg. 1 for next)
|
||||||
|
*/
|
||||||
|
public findNextCourse(indexOffset: number)
|
||||||
|
{
|
||||||
|
return this.findCourse(this.activeIndex.split('/')[1], indexOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find course
|
||||||
|
*
|
||||||
|
* @param courseId Course ID
|
||||||
|
* @param indexOffset Index offset (Eg. 1 for next)
|
||||||
|
*/
|
||||||
|
public findCourse(courseId: string, indexOffset: number)
|
||||||
|
{
|
||||||
|
// Find current course index
|
||||||
|
let courseIndex = this.courses.findIndex(c => c.id == +courseId);
|
||||||
|
|
||||||
|
// Find next course
|
||||||
|
return this.courses[courseIndex + indexOffset];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select grading period
|
||||||
|
*
|
||||||
|
* @param command Term 1, Term 2, All Year, etc.
|
||||||
|
*/
|
||||||
|
public selectGradingPeriod(command: string)
|
||||||
|
{
|
||||||
|
this.gradingPeriod = command;
|
||||||
|
this.$cookies.set('va.grading-period', command, '10y');
|
||||||
|
|
||||||
|
// Call event
|
||||||
|
this.$emit('select-time', this.getSelectedGradingPeriod());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get code for selected time
|
||||||
|
*/
|
||||||
|
public getSelectedGradingPeriod(): number
|
||||||
|
{
|
||||||
|
if (this.gradingPeriod == 'All Year') return -1;
|
||||||
|
else return +this.gradingPeriod.replace('Term ', '') - 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,49 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="navigation">
|
<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="activeIndex" @select="onSelect">
|
||||||
|
|
||||||
|
<div id="nav-title">
|
||||||
|
Veracross Analyzer
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-menu-item index="overall">Overall</el-menu-item>
|
<el-menu-item index="overall">Overall</el-menu-item>
|
||||||
|
|
||||||
<el-submenu index="courses">
|
<el-submenu index="courses">
|
||||||
<template slot="title">Courses</template>
|
<template slot="title">Courses</template>
|
||||||
<el-menu-item v-for="course in courses"
|
<el-menu-item v-for="course in courses"
|
||||||
:index="`course-${course.name}`"
|
:index="formatCourseIndex(course)"
|
||||||
:key="course.name">{{course.name}}</el-menu-item>
|
:key="course.name">{{course.name}}</el-menu-item>
|
||||||
</el-submenu>
|
</el-submenu>
|
||||||
|
|
||||||
<el-button @click="signOut" id="sign-out-button" type="text">Sign Out</el-button>
|
<!-- Grading period selection -->
|
||||||
|
<el-dropdown id="nav-grading-period" @command="selectGradingPeriod">
|
||||||
|
<el-button type="primary" size="medium">
|
||||||
|
{{gradingPeriod}}<i class="el-icon-arrow-down el-icon--right"></i>
|
||||||
|
</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" disabled>Term 3</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="Term 4" disabled>Term 4</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="All Year" divided>All Year</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</el-dropdown>
|
||||||
|
|
||||||
|
<el-button @click="$emit('sign-out')" id="sign-out-button" type="text">Sign Out</el-button>
|
||||||
</el-menu>
|
</el-menu>
|
||||||
<div class="line"></div>
|
|
||||||
|
<!-- Previous course / Next course (Only when the page is courses) -->
|
||||||
|
<div v-if="activeIndex.includes('course') && findNextCourse(-1) != null"
|
||||||
|
@click="nextCourse(-1)" id="prev-course" class="nav-course-operations unselectable">
|
||||||
|
▲ PREVIOUS COURSE ▲
|
||||||
|
</div>
|
||||||
|
<footer>
|
||||||
|
<div v-if="activeIndex.includes('course') && findNextCourse(1) != null"
|
||||||
|
@click="nextCourse(1)" id="next-course" class="nav-course-operations unselectable">
|
||||||
|
▼ NEXT COURSE ▼
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
+48
-4
@@ -3,15 +3,59 @@
|
|||||||
*/
|
*/
|
||||||
export default class Constants
|
export default class Constants
|
||||||
{
|
{
|
||||||
/**
|
/** Base url for api access */
|
||||||
* Base url for api access
|
|
||||||
*/
|
|
||||||
public static API_URL: string = 'https://va.hydev.org/api';
|
public static API_URL: string = 'https://va.hydev.org/api';
|
||||||
|
|
||||||
|
/** Current version */
|
||||||
|
public static VERSION: string = '0.4.2.912';
|
||||||
|
|
||||||
|
/** Minimum version that still supports the same cookies */
|
||||||
|
public static MIN_SUPPORTED_VERSION: string = '0.3.4.561';
|
||||||
|
|
||||||
|
public static GITHUB: string = 'https://github.com/HyDevelop/VeracrossAnalyzer.Client';
|
||||||
|
|
||||||
public static SPLASH: string =
|
public static SPLASH: string =
|
||||||
'. , ,---. | \n' +
|
'. , ,---. | \n' +
|
||||||
'| |. , |---|,---.,---.| , .,---,,---.,---.\n' +
|
'| |. , |---|,---.,---.| , .,---,,---.,---.\n' +
|
||||||
' \\ / >< | || |,---|| | | .-\' |---\'| \n' +
|
' \\ / >< | || |,---|| | | .-\' |---\'| \n' +
|
||||||
' `\' \' ` ` \'` \'`---^`---\'`---|\'---\'`---\'` \n' +
|
' `\' \' ` ` \'` \'`---^`---\'`---|\'---\'`---\'` \n' +
|
||||||
' v0.1.2.236 `---\' '
|
' `---\' \n' +
|
||||||
|
` Version v${Constants.VERSION} by Hykilpikonna (YGui21)\n` +
|
||||||
|
` Github: ${Constants.GITHUB}`;
|
||||||
|
|
||||||
|
// Graph Theme
|
||||||
|
public 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
|
||||||
|
public static TERMS = [new Date('Sep 04 2019'), new Date('Nov 01 2019'), new Date('Feb 02 2020')];
|
||||||
|
public static CURRENT_TERM = 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
import {Assignment} from '@/components/app/app';
|
||||||
|
import JsonUtils from '@/utils/json-utils';
|
||||||
|
import {FormatUtils} from '@/utils/format-utils';
|
||||||
|
import {CourseUtils} from '@/utils/course-utils';
|
||||||
|
import Navigation from '@/components/navigation/navigation';
|
||||||
|
import {GPAUtils} from '@/utils/gpa-utils';
|
||||||
|
|
||||||
|
export default class Course
|
||||||
|
{
|
||||||
|
id: number;
|
||||||
|
assignmentsId: number;
|
||||||
|
name: string;
|
||||||
|
teacherName: string;
|
||||||
|
status: string;
|
||||||
|
rawAssignments: Assignment[];
|
||||||
|
|
||||||
|
rawLetterGrade?: string;
|
||||||
|
rawNumericGrade?: number;
|
||||||
|
|
||||||
|
level: string;
|
||||||
|
scaleUp: number;
|
||||||
|
|
||||||
|
grading:
|
||||||
|
{
|
||||||
|
method: string
|
||||||
|
weightingMap: {[index: string]: number}
|
||||||
|
};
|
||||||
|
|
||||||
|
computed:
|
||||||
|
{
|
||||||
|
termAssignments: Assignment[][]
|
||||||
|
allYearGrade: number
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a course with a course json object
|
||||||
|
*
|
||||||
|
* @param courseJson Course json object
|
||||||
|
*/
|
||||||
|
constructor(courseJson: any)
|
||||||
|
{
|
||||||
|
this.id = courseJson.id;
|
||||||
|
this.assignmentsId = courseJson.assignmentsId;
|
||||||
|
this.name = FormatUtils.parseText(courseJson.name).trim();
|
||||||
|
this.teacherName = courseJson.teacherName;
|
||||||
|
this.status = courseJson.status;
|
||||||
|
|
||||||
|
this.rawLetterGrade = courseJson.letterGrade;
|
||||||
|
this.rawNumericGrade = courseJson.numericGrade;
|
||||||
|
|
||||||
|
// Other api issue
|
||||||
|
if (this.rawLetterGrade == '')
|
||||||
|
{
|
||||||
|
this.rawNumericGrade = undefined;
|
||||||
|
this.rawLetterGrade = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Level and scaleUp
|
||||||
|
let level = CourseUtils.detectLevel(this.name);
|
||||||
|
if (level != undefined)
|
||||||
|
{
|
||||||
|
this.level = level.level;
|
||||||
|
this.scaleUp = level.scaleUp;
|
||||||
|
}
|
||||||
|
else this.level = 'Unknown';
|
||||||
|
|
||||||
|
this.grading = courseJson.grading;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load in assignments data
|
||||||
|
*
|
||||||
|
* @param data Assignments data
|
||||||
|
*/
|
||||||
|
loadAssignments(data: any)
|
||||||
|
{
|
||||||
|
// Load assignments
|
||||||
|
// Parse json and filter it
|
||||||
|
this.rawAssignments = JsonUtils.filterAssignments(data);
|
||||||
|
|
||||||
|
// Sort by date (Latest is at 0)
|
||||||
|
this.rawAssignments.sort((a, b) => b.date.getTime() - a.date.getTime());
|
||||||
|
|
||||||
|
// Filter assignments into terms
|
||||||
|
let termAssignments: Assignment[][] = [[], [], [], []];
|
||||||
|
let currentTerm = 0;
|
||||||
|
|
||||||
|
// Loop through it by time order
|
||||||
|
this.rawAssignments.forEach(a => termAssignments[a.gradingPeriod].push(a));
|
||||||
|
|
||||||
|
// Set computed data
|
||||||
|
this.computed = {termAssignments: termAssignments, allYearGrade: -1};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.filter(a => a.complete == 'Complete').length == 0) return false;
|
||||||
|
|
||||||
|
// Skip if there are no grading scale
|
||||||
|
// if (course.grading.method == 'NOT_GRADED') return;
|
||||||
|
|
||||||
|
// Is graded
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get assignments of the selected time
|
||||||
|
*/
|
||||||
|
get assignments(): Assignment[]
|
||||||
|
{
|
||||||
|
let timeCode = Navigation.instance.getSelectedGradingPeriod();
|
||||||
|
|
||||||
|
// All year
|
||||||
|
if (timeCode == -1)
|
||||||
|
{
|
||||||
|
return this.rawAssignments;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Specific time
|
||||||
|
return this.computed.termAssignments[timeCode];
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Optimize this
|
||||||
|
private letterGradeComputed = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get letter grade
|
||||||
|
*/
|
||||||
|
get letterGrade(): string
|
||||||
|
{
|
||||||
|
// Cached
|
||||||
|
if (this.rawLetterGrade != undefined && this.letterGradeComputed)
|
||||||
|
return this.rawLetterGrade;
|
||||||
|
this.letterGradeComputed = true;
|
||||||
|
|
||||||
|
// Get scale
|
||||||
|
let scale = GPAUtils.findScale(this.numericGrade);
|
||||||
|
|
||||||
|
// Scale not found
|
||||||
|
if (scale == undefined) return this.rawLetterGrade = '--';
|
||||||
|
|
||||||
|
// Return
|
||||||
|
return this.rawLetterGrade = scale.letter;
|
||||||
|
}
|
||||||
|
|
||||||
|
private numericGradeComputed = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get numeric grade
|
||||||
|
*/
|
||||||
|
get numericGrade(): number
|
||||||
|
{
|
||||||
|
// Cached
|
||||||
|
if (this.rawNumericGrade != undefined && this.numericGradeComputed)
|
||||||
|
return this.rawNumericGrade;
|
||||||
|
this.numericGradeComputed = true;
|
||||||
|
|
||||||
|
// Calculate
|
||||||
|
if (this.grading.method == 'PERCENT_TYPE')
|
||||||
|
{
|
||||||
|
return this.rawNumericGrade = GPAUtils.getPercentTypeAverage(this, this.assignments);
|
||||||
|
}
|
||||||
|
if (this.grading.method == 'TOTAL_MEAN')
|
||||||
|
{
|
||||||
|
return this.rawNumericGrade = GPAUtils.getTotalMeanAverage(this.assignments);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -1,10 +1,10 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import ElementUI from 'element-ui';
|
import ElementUI from 'element-ui';
|
||||||
const VCharts = require('v-charts');
|
|
||||||
|
|
||||||
import App from './components/app/app.vue';
|
import App from './components/app/app.vue';
|
||||||
import VueCookies from 'vue-cookies';
|
import VueCookies from 'vue-cookies';
|
||||||
|
|
||||||
|
const VCharts = require('v-charts');
|
||||||
|
|
||||||
Vue.config.productionTip = false;
|
Vue.config.productionTip = false;
|
||||||
|
|
||||||
// Use Element UI
|
// Use Element UI
|
||||||
|
|||||||
@@ -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">{{typeName}}</span>
|
||||||
|
<span id="type-average">Average: {{average.toFixed(2)}}%</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AssignmentEntry v-for="assignment of filteredAssignments" :key="assignment.id"
|
||||||
|
:assignment="assignment" :unread="false"
|
||||||
|
backgroundColor="#ffffff" narrow="true">
|
||||||
|
</AssignmentEntry>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||||
|
import {Assignment} from '@/components/app/app';
|
||||||
|
import AssignmentEntry from '@/pages/overall/overall-course/assignment-entry/assignment-entry.vue';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: {AssignmentEntry}
|
||||||
|
})
|
||||||
|
export default class AssignmentTypeHead extends Vue
|
||||||
|
{
|
||||||
|
@Prop({required: true}) typeName: string;
|
||||||
|
@Prop({required: true}) assignments: Assignment[];
|
||||||
|
|
||||||
|
get filteredAssignments()
|
||||||
|
{
|
||||||
|
// Filter assignments to only this type
|
||||||
|
return this.assignments.filter(a => a.complete == 'Complete' && a.type == this.typeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
get average()
|
||||||
|
{
|
||||||
|
return this.filteredAssignments.reduce((a, b) => a + b.score, 0) /
|
||||||
|
this.filteredAssignments.reduce((a, b) => a + b.scoreMax, 0) * 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
#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,20 @@
|
|||||||
|
// 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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<template>
|
||||||
|
<el-card id="course-card" class="course-card">
|
||||||
|
<course-head :clickable="false" :course="course" :unread="countUnread()"></course-head>
|
||||||
|
|
||||||
|
<div class="course-card-content expand">
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-card class="large overall-line-card vertical-center">
|
||||||
|
<course-scatter :course="course"></course-scatter>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="0">
|
||||||
|
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<AssignmentTypeHead v-for="type in getAssignmentTypes()" :key="type"
|
||||||
|
:type-name="type" :assignments="course.assignments">
|
||||||
|
</AssignmentTypeHead>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||||
|
import {Assignment} from '@/components/app/app';
|
||||||
|
import CourseHead from '@/pages/overall/overall-course/course-head/course-head.vue';
|
||||||
|
import CourseScatter from '@/pages/course/course-scatter/course-scatter';
|
||||||
|
import AssignmentEntry from '@/pages/overall/overall-course/assignment-entry/assignment-entry.vue';
|
||||||
|
import AssignmentTypeHead from '@/pages/course/assignment-type-head/assignment-type-head.vue';
|
||||||
|
import Course from '@/logic/course';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: {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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all the types of the assignments.
|
||||||
|
*/
|
||||||
|
getAssignmentTypes(): string[]
|
||||||
|
{
|
||||||
|
// Get all types
|
||||||
|
let types = this.course.assignments.map(a => a.type);
|
||||||
|
|
||||||
|
// Remove duplicates
|
||||||
|
types = types.filter((type, i, a) => a.indexOf(type) == i);
|
||||||
|
|
||||||
|
// Return it
|
||||||
|
return types;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style src="./course-page.scss" lang="scss" scoped></style>
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||||
|
import {Assignment} from '@/components/app/app';
|
||||||
|
import Constants from '@/constants';
|
||||||
|
import {FormatUtils} from '@/utils/format-utils';
|
||||||
|
import moment from 'moment';
|
||||||
|
import Course from '@/logic/course';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
})
|
||||||
|
export default class CourseScatter extends Vue
|
||||||
|
{
|
||||||
|
private static DOT = '<span style="display:inline-block;margin-right:5px;border-radius:10px;width:9px;height:9px;background-color:{color}"></span>';
|
||||||
|
|
||||||
|
@Prop({required: true}) course: Course;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override options
|
||||||
|
*
|
||||||
|
* @param options Original options (Unused)
|
||||||
|
*/
|
||||||
|
afterConfig(options: any)
|
||||||
|
{
|
||||||
|
return this.chartSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate settings
|
||||||
|
*/
|
||||||
|
get chartSettings()
|
||||||
|
{
|
||||||
|
// Map assignments
|
||||||
|
let map = this.mapAssignments();
|
||||||
|
|
||||||
|
// Scatter data point style
|
||||||
|
let itemStyle =
|
||||||
|
{
|
||||||
|
normal:
|
||||||
|
{
|
||||||
|
opacity: 0.8,
|
||||||
|
shadowBlur: 10,
|
||||||
|
shadowOffsetX: 0,
|
||||||
|
shadowOffsetY: 0,
|
||||||
|
shadowColor: 'rgba(0, 0, 0, 0.2)'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create settings
|
||||||
|
let settings =
|
||||||
|
{
|
||||||
|
// Color
|
||||||
|
color: Constants.THEME.colors,
|
||||||
|
|
||||||
|
// Title
|
||||||
|
title:
|
||||||
|
{
|
||||||
|
show: true,
|
||||||
|
textStyle:
|
||||||
|
{
|
||||||
|
fontSize: 13
|
||||||
|
},
|
||||||
|
text: 'Assignments',
|
||||||
|
subtext: 'Assignment scores for ' + this.course.name,
|
||||||
|
x: 'center'
|
||||||
|
},
|
||||||
|
|
||||||
|
// X axis represents course names
|
||||||
|
xAxis:
|
||||||
|
{
|
||||||
|
type: 'time',
|
||||||
|
axisLabel:
|
||||||
|
{
|
||||||
|
formatter: (name: any) => moment(name).format('MMM DD')
|
||||||
|
},
|
||||||
|
max: FormatUtils.toChartDate(new Date())
|
||||||
|
},
|
||||||
|
|
||||||
|
// Y axis represents GPAs and MaxGPAs
|
||||||
|
yAxis:
|
||||||
|
{
|
||||||
|
type: 'value',
|
||||||
|
name: 'Percentage Score',
|
||||||
|
nameLocation: 'center',
|
||||||
|
nameGap: 38,
|
||||||
|
axisLabel:
|
||||||
|
{
|
||||||
|
formatter: (name: any) => name + '%'
|
||||||
|
},
|
||||||
|
max: 100,
|
||||||
|
min: (value: any) => Math.floor(value.min) - 5
|
||||||
|
},
|
||||||
|
|
||||||
|
// Tooltip
|
||||||
|
tooltip:
|
||||||
|
{
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer:
|
||||||
|
{
|
||||||
|
type: 'cross'
|
||||||
|
},
|
||||||
|
formatter: (ps: any[]) => ps[0].data[0] + '<br>' + ps.map(p =>
|
||||||
|
`${CourseScatter.DOT.replace('{color}', p.color)}
|
||||||
|
${FormatUtils.limit(p.data[2], 22)}: ${p.data[1]}%<br>`).join('')
|
||||||
|
},
|
||||||
|
|
||||||
|
// Legend
|
||||||
|
legend:
|
||||||
|
{
|
||||||
|
bottom: 24,
|
||||||
|
itemWidth: 14,
|
||||||
|
textStyle:
|
||||||
|
{
|
||||||
|
color: '#777',
|
||||||
|
fontSize: 11
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Data
|
||||||
|
series: Array.from(map, ([type, assignments]) =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
type: 'scatter',
|
||||||
|
name: type,
|
||||||
|
data: CourseScatter.assignmentsData(assignments),
|
||||||
|
itemStyle: itemStyle
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map assignments to {assignmentType, [assignment]} format.
|
||||||
|
*/
|
||||||
|
private mapAssignments(): Map<string, Assignment[]>
|
||||||
|
{
|
||||||
|
// Define map
|
||||||
|
let map = new Map();
|
||||||
|
|
||||||
|
// Move data to map
|
||||||
|
this.course.assignments.forEach(a =>
|
||||||
|
{
|
||||||
|
// Null case, create empty array
|
||||||
|
if (!map.has(a.type)) map.set(a.type, []);
|
||||||
|
|
||||||
|
// Put data
|
||||||
|
map.get(a.type).push(a);
|
||||||
|
});
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert assignments to series data
|
||||||
|
*
|
||||||
|
* @param assignments Assignments
|
||||||
|
*/
|
||||||
|
private static assignmentsData(assignments: Assignment[])
|
||||||
|
{
|
||||||
|
return assignments.filter(a => a.complete == 'Complete')
|
||||||
|
.map(a => [FormatUtils.toChartDate(a.date), (a.score / a.scoreMax * 100).toFixed(2), a.description]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<template>
|
||||||
|
<div id="course-scatter">
|
||||||
|
<ve-scatter height="450px" class="graph" :extend="{heyIUsedCourseObject: this.course.name}" :after-config="afterConfig"></ve-scatter>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./course-scatter.ts" lang="ts"></script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
#overall-bar
|
||||||
|
{
|
||||||
|
.graph
|
||||||
|
{
|
||||||
|
margin-top: 50px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -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,117 @@
|
|||||||
|
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||||
|
import Course from '@/logic/course';
|
||||||
|
import {GPAUtils} from '@/utils/gpa-utils';
|
||||||
|
import Constants from '@/constants';
|
||||||
|
import {FormatUtils} from '@/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,17 @@
|
|||||||
|
<template>
|
||||||
|
<div id="overall-bar">
|
||||||
|
<ve-bar height="450px" class="graph"
|
||||||
|
:extend="chartSettings"></ve-bar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./overall-bar.ts" lang="ts"></script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
#overall-bar
|
||||||
|
{
|
||||||
|
.graph
|
||||||
|
{
|
||||||
|
margin-top: 50px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
|
||||||
|
// Row
|
||||||
|
.assignment-entry
|
||||||
|
{
|
||||||
|
height: 40px;
|
||||||
|
padding: 0 10px 0 20px;
|
||||||
|
background: #f5f7fa;
|
||||||
|
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
// Date
|
||||||
|
.el-col.date
|
||||||
|
{
|
||||||
|
min-width: 130px;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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,71 @@
|
|||||||
|
<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.date).format("MMM D")}}</span>
|
||||||
|
<span class="now">({{getMoment(assignment.date).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 class="percent entry-box">
|
||||||
|
{{(assignment.score / assignment.scoreMax * 100).toFixed(1)}}
|
||||||
|
<span class="symbol">%</span>
|
||||||
|
</span>
|
||||||
|
<span class="score entry-box">{{assignment.score}}</span>
|
||||||
|
<span 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 {Assignment} from '@/components/app/app';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
@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: string)
|
||||||
|
{
|
||||||
|
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></style>
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
// Main card content
|
||||||
|
.course-card-content.main
|
||||||
|
{
|
||||||
|
padding: 0 20px 0 20px;
|
||||||
|
height: 90px;
|
||||||
|
|
||||||
|
// Main color
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-col-name
|
||||||
|
{
|
||||||
|
// Align left
|
||||||
|
text-align: left;
|
||||||
|
float: left;
|
||||||
|
|
||||||
|
.course-name
|
||||||
|
{
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 22px;
|
||||||
|
color: var(--main);
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-teacher
|
||||||
|
{
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999999;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-col-grade
|
||||||
|
{
|
||||||
|
// Align right
|
||||||
|
text-align: right;
|
||||||
|
float: right;
|
||||||
|
|
||||||
|
// Adjust position
|
||||||
|
margin-top: -2px;
|
||||||
|
|
||||||
|
.course-grade
|
||||||
|
{
|
||||||
|
font-size: 21px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-updates.unread
|
||||||
|
{
|
||||||
|
.unread-number
|
||||||
|
{
|
||||||
|
background: var(--unread);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unread-text
|
||||||
|
{
|
||||||
|
color: var(--unread);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-updates.none
|
||||||
|
{
|
||||||
|
color: #999999;
|
||||||
|
|
||||||
|
.unread-number
|
||||||
|
{
|
||||||
|
background: #eeeeee;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<template>
|
||||||
|
<div id="course-head" class="course-card-content main vertical-center">
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="12" class="course-col-name">
|
||||||
|
<div v-if="clickable" class="course-name clickable" @click="redirect">{{course.name}}</div>
|
||||||
|
<div v-if="!clickable" class="course-name">{{course.name}}</div>
|
||||||
|
<div class="course-teacher">{{course.teacherName}}</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12" class="course-col-grade">
|
||||||
|
<div class="course-grade">
|
||||||
|
<span class="letter">{{course.letterGrade}} </span>
|
||||||
|
<span class="numeric">{{course.numericGrade === undefined ? '--' : course.numericGrade.toFixed(2)}}</span>
|
||||||
|
<span class="percent" v-if="course.numericGrade !== undefined">%</span>
|
||||||
|
</div>
|
||||||
|
<div class="course-updates" @click="redirect" :class="unread === 0 ? 'none' : 'unread'">
|
||||||
|
<span class="unread-number">{{unread}}</span>
|
||||||
|
<span class="unread-text" :class="clickable ? 'clickable' : ''">
|
||||||
|
new update{{unread >= 2 ? 's' : ''}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||||
|
import Course from '@/logic/course';
|
||||||
|
import {CourseUtils} from '@/utils/course-utils';
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirect to the course page
|
||||||
|
*/
|
||||||
|
redirect()
|
||||||
|
{
|
||||||
|
if (!this.clickable) return;
|
||||||
|
Navigation.instance.updateIndex(CourseUtils.formatTabIndex(this.course));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style src="./course-head.scss" lang="scss"></style>
|
||||||
@@ -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,51 @@
|
|||||||
|
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||||
|
import App, {Assignment} from '@/components/app/app';
|
||||||
|
import AssignmentEntry from '@/pages/overall/overall-course/assignment-entry/assignment-entry.vue';
|
||||||
|
import CourseHead from '@/pages/overall/overall-course/course-head/course-head.vue';
|
||||||
|
import Course from '@/logic/course';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: {UnreadEntry: AssignmentEntry, CourseHead}
|
||||||
|
})
|
||||||
|
export default class OverallCourse 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark an assignment as read
|
||||||
|
*/
|
||||||
|
markAsRead(assignment: Assignment)
|
||||||
|
{
|
||||||
|
App.http.post('/mark-as-read', {scoreId: assignment.scoreId})
|
||||||
|
.then(response =>
|
||||||
|
{
|
||||||
|
// Check success
|
||||||
|
if (response.success)
|
||||||
|
{
|
||||||
|
this.unreadAssignments = this.unreadAssignments.filter(a => a != assignment);
|
||||||
|
this.unread = this.unreadAssignments.length;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Show error message TODO: Show it properly
|
||||||
|
alert(response.data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<template>
|
||||||
|
<div id="overall-course">
|
||||||
|
<el-card class="course-card">
|
||||||
|
<course-head :clickable="true" :course="course" :unread="countUnread()"></course-head>
|
||||||
|
<div class="course-card-content expand"
|
||||||
|
v-if="countUnread() !== 0">
|
||||||
|
<unread-entry v-for="assignment in unreadAssignments"
|
||||||
|
:assignment="assignment"
|
||||||
|
:key="assignment.id"
|
||||||
|
unread="true"
|
||||||
|
v-on:mark-as-read="markAsRead">
|
||||||
|
</unread-entry>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./overall-course.ts" lang="ts"></script>
|
||||||
|
<style src="./overall-course.scss" lang="scss" scoped></style>
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||||
|
import moment from 'moment';
|
||||||
|
import Course from '@/logic/course';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
})
|
||||||
|
export default class OverallLine extends Vue
|
||||||
|
{
|
||||||
|
@Prop({required: true}) courses: Course[];
|
||||||
|
|
||||||
|
private settings =
|
||||||
|
{
|
||||||
|
// Title
|
||||||
|
title:
|
||||||
|
{
|
||||||
|
show: true,
|
||||||
|
textStyle:
|
||||||
|
{
|
||||||
|
fontSize: 12
|
||||||
|
},
|
||||||
|
text: 'Average Grade',
|
||||||
|
subtext: 'Average score trend for every course',
|
||||||
|
x: 'center'
|
||||||
|
},
|
||||||
|
// Legend
|
||||||
|
legend:
|
||||||
|
{
|
||||||
|
show: false,
|
||||||
|
textStyle:
|
||||||
|
{
|
||||||
|
fontSize: 11
|
||||||
|
},
|
||||||
|
icon: 'circle'
|
||||||
|
},
|
||||||
|
// Zoom bar
|
||||||
|
dataZoom:
|
||||||
|
[
|
||||||
|
{
|
||||||
|
startValue: moment().subtract(30, 'days').format('M/D/YYYY')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'inside'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
series:
|
||||||
|
{
|
||||||
|
smooth: true,
|
||||||
|
|
||||||
|
// Mark area
|
||||||
|
markArea:
|
||||||
|
{
|
||||||
|
silent: true,
|
||||||
|
data:
|
||||||
|
[
|
||||||
|
// Above 100
|
||||||
|
[
|
||||||
|
{
|
||||||
|
yAxis: 120,
|
||||||
|
itemStyle: {color: 'rgba(230,253,255,0.09)'}
|
||||||
|
}, {yAxis: 100}
|
||||||
|
],
|
||||||
|
// 90 to 100
|
||||||
|
[
|
||||||
|
{
|
||||||
|
yAxis: 100,
|
||||||
|
itemStyle: {color: 'rgba(241,255,237,0.09)'}
|
||||||
|
}, {yAxis: 90}
|
||||||
|
],
|
||||||
|
// 80 to 90
|
||||||
|
[
|
||||||
|
{
|
||||||
|
yAxis: 90,
|
||||||
|
itemStyle: {color: 'rgba(255,250,216,0.09)'}
|
||||||
|
}, {yAxis: 80}
|
||||||
|
],
|
||||||
|
// 70 to 80
|
||||||
|
[
|
||||||
|
{
|
||||||
|
yAxis: 80,
|
||||||
|
itemStyle: {color: 'rgba(255,225,199,0.1)'}
|
||||||
|
}, {yAxis: 70}
|
||||||
|
],
|
||||||
|
// Below 70 (Fail)
|
||||||
|
[
|
||||||
|
{
|
||||||
|
yAxis: 70,
|
||||||
|
itemStyle: {color: 'rgb(255,190,184, 0.09)'}
|
||||||
|
}, {yAxis: -100}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
xAxis:
|
||||||
|
{
|
||||||
|
//type: 'time'
|
||||||
|
},
|
||||||
|
yAxis:
|
||||||
|
{
|
||||||
|
min: (value: any) => Math.floor(value.min),
|
||||||
|
max: (value: any) => Math.min(value.max, 110)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
chartCache: any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert assignments list to a graph dataset.
|
||||||
|
*/
|
||||||
|
get convertChart()
|
||||||
|
{
|
||||||
|
// Caching
|
||||||
|
if (this.chartCache != undefined) return this.chartCache;
|
||||||
|
|
||||||
|
let courses = this.courses.filter(c => c.assignments.length > 0);
|
||||||
|
|
||||||
|
// Compute the column names
|
||||||
|
let columns = courses.map(course => course.name);
|
||||||
|
columns.unshift('date');
|
||||||
|
|
||||||
|
// Find the min date
|
||||||
|
let minDates = courses.map(course => course.assignments[course.assignments.length - 1].date.getTime());
|
||||||
|
let minDate: Date = new Date(Math.min.apply(null, minDates));
|
||||||
|
|
||||||
|
// Find the dates in between
|
||||||
|
let now = new Date();
|
||||||
|
let dates = [];
|
||||||
|
for (let date = minDate; date <= now; date.setDate(date.getDate() + 1))
|
||||||
|
{
|
||||||
|
dates.push(new Date(date));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute the rows data
|
||||||
|
let rows: {[index: string]: any}[] = [];
|
||||||
|
dates.forEach(date =>
|
||||||
|
{
|
||||||
|
// Define row object
|
||||||
|
let row: {[index: string]:any} = {'date': date.toLocaleDateString('en-US')};
|
||||||
|
|
||||||
|
// Loop through courses
|
||||||
|
courses.forEach(course =>
|
||||||
|
{
|
||||||
|
// Total Mean
|
||||||
|
if (course.grading.method == 'TOTAL_MEAN')
|
||||||
|
{
|
||||||
|
let score = 0;
|
||||||
|
let max = 0;
|
||||||
|
|
||||||
|
// Loop through assignments
|
||||||
|
course.assignments.forEach(assignment =>
|
||||||
|
{
|
||||||
|
// If assignment should be displayed
|
||||||
|
if (assignment.complete != 'Complete') return;
|
||||||
|
|
||||||
|
// Date is being looked at
|
||||||
|
if (assignment.date.getTime() < date.getTime())
|
||||||
|
{
|
||||||
|
// Record scores
|
||||||
|
score += assignment.score;
|
||||||
|
max += assignment.scoreMax;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add average to the row
|
||||||
|
row[course.name] = score / max * 100;
|
||||||
|
}
|
||||||
|
else if (course.grading.method == 'PERCENT_TYPE')
|
||||||
|
{
|
||||||
|
let typeScores: {[index: string]: any} = {};
|
||||||
|
let typeCounts: {[index: string]: any} = {};
|
||||||
|
|
||||||
|
// Loop through assignments
|
||||||
|
course.assignments.forEach(assignment =>
|
||||||
|
{
|
||||||
|
// If assignment should be displayed
|
||||||
|
if (assignment.complete != 'Complete') return;
|
||||||
|
|
||||||
|
// Date is being looked at
|
||||||
|
if (assignment.date.getTime() < date.getTime())
|
||||||
|
{
|
||||||
|
// Record scores
|
||||||
|
if (typeScores[assignment.type] == undefined) typeScores[assignment.type] = 0;
|
||||||
|
typeScores[assignment.type] += assignment.score / assignment.scoreMax;
|
||||||
|
|
||||||
|
if (typeCounts[assignment.type] == undefined) typeCounts[assignment.type] = 0;
|
||||||
|
typeCounts[assignment.type] ++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Count total percentage (This is to avoid less than expected cases)
|
||||||
|
// Eg. If HW = 25% and Quiz = 75%, I have 1 hw and 0 quiz
|
||||||
|
// Without total percentage, the avg grade I get is 25%.
|
||||||
|
let totalPercentage = 0;
|
||||||
|
for (let type in course.grading.weightingMap)
|
||||||
|
{
|
||||||
|
if (typeScores[type] != undefined)
|
||||||
|
{
|
||||||
|
totalPercentage += course.grading.weightingMap[type];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count
|
||||||
|
let score = 0;
|
||||||
|
for (let type in typeScores)
|
||||||
|
{
|
||||||
|
let typeFactor = course.grading.weightingMap[type] / totalPercentage;
|
||||||
|
score += typeScores[type] * typeFactor / typeCounts[type];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add average to the row
|
||||||
|
if (score != 0) row[course.name] = score * 100;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add it to the array
|
||||||
|
rows.push(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.chartCache =
|
||||||
|
{
|
||||||
|
columns: columns,
|
||||||
|
rows: rows
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<template>
|
||||||
|
<div id="overall-line">
|
||||||
|
<ve-line :data="convertChart" :extend="settings"></ve-line>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./overall-line.ts" lang="ts"></script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -1,17 +1,4 @@
|
|||||||
|
|
||||||
// Add some margins
|
|
||||||
.el-card
|
|
||||||
{
|
|
||||||
margin: 10px;
|
|
||||||
height: 494px;
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
// Vertical center
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gpa-card
|
.gpa-card
|
||||||
{
|
{
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
@@ -31,7 +18,7 @@
|
|||||||
.gpa.text
|
.gpa.text
|
||||||
{
|
{
|
||||||
font-size: 35px;
|
font-size: 35px;
|
||||||
font-family: 'Avenir', Helvetica, Arial, sans-serif;
|
font-family: var(--font);
|
||||||
}
|
}
|
||||||
|
|
||||||
.gpa.max
|
.gpa.max
|
||||||
@@ -46,3 +33,21 @@
|
|||||||
{
|
{
|
||||||
font-size: 11px;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,155 +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[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert assignments list to a graph dataset.
|
|
||||||
*/
|
|
||||||
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 since I can't import another
|
|
||||||
* class in the Vue file.
|
|
||||||
*/
|
|
||||||
public getGPA()
|
|
||||||
{
|
|
||||||
let gpa = GPAUtils.getGPA(this.courses);
|
|
||||||
// let result = '' + gpa.gpa;
|
|
||||||
|
|
||||||
/* Not accurate
|
|
||||||
if (!gpa.accurate)
|
|
||||||
{
|
|
||||||
result = `(${result})`;
|
|
||||||
}*/
|
|
||||||
|
|
||||||
return gpa;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="overall">
|
<div id="overall">
|
||||||
<el-row>
|
<el-row v-if="getGPA().gpa !== -1">
|
||||||
<el-col :span="4">
|
<el-col :span="4">
|
||||||
<el-card class="gpa-card">
|
<el-card class="large gpa-card vertical-center">
|
||||||
<div style="padding: 14px;">
|
<div style="padding: 14px;">
|
||||||
<span class="gpa header">GPA:</span>
|
<span class="gpa header">GPA:</span>
|
||||||
<span class="gpa text">{{getGPA().gpa}}</span>
|
<span class="gpa text">{{getGPA().gpa}}</span>
|
||||||
@@ -13,15 +13,55 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="20">
|
<el-col :span="14">
|
||||||
<el-card style="margin-right: 20px">
|
<el-card class="large overall-line-card vertical-center">
|
||||||
<graph-overall :chart="convertCharts"></graph-overall>
|
<overall-line :courses="courses"></overall-line>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card class="large overall-bar-card vertical-center">
|
||||||
|
<overall-bar :courses="courses"></overall-bar>
|
||||||
</el-card>
|
</el-card>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</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">
|
||||||
|
</overall-course>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./overall.ts" lang="ts"></script>
|
<script lang="ts">
|
||||||
<style src="./overall.scss" lang="scss"></style>
|
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';
|
||||||
|
import Course from '@/logic/course';
|
||||||
|
import {GPAUtils} from '@/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.
|
||||||
|
*/
|
||||||
|
public getGPA()
|
||||||
|
{
|
||||||
|
return GPAUtils.getGPA(this.courses);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style src="./overall.scss" lang="scss" scoped></style>
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import Vue, { VNode } from 'vue';
|
import Vue, {VNode} from 'vue';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
namespace JSX {
|
namespace JSX {
|
||||||
|
|||||||
Vendored
+4
-3
@@ -1,4 +1,5 @@
|
|||||||
declare module '*.vue' {
|
declare module '*.vue'
|
||||||
import Vue from 'vue';
|
{
|
||||||
export default Vue;
|
import Vue from 'vue';
|
||||||
|
export default Vue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import Course from '@/logic/course';
|
||||||
|
|
||||||
|
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 UNKNOWN_COURSE_LIST = new Map();
|
||||||
|
UNKNOWN_COURSE_LIST.set('Piano Masterclass', LEVEL_H);
|
||||||
|
UNKNOWN_COURSE_LIST.set('Multivariable Calculus with Differential Equations', LEVEL_H);
|
||||||
|
UNKNOWN_COURSE_LIST.set('Introduction to Algorithmic Thinking and Computational Technologies', LEVEL_A);
|
||||||
|
UNKNOWN_COURSE_LIST.set('Ceramics 1', LEVEL_CP);
|
||||||
|
UNKNOWN_COURSE_LIST.set('Ceramics 2', LEVEL_A);
|
||||||
|
UNKNOWN_COURSE_LIST.set('Sculpture', LEVEL_CP);
|
||||||
|
UNKNOWN_COURSE_LIST.set('Drawing', LEVEL_CP);
|
||||||
|
UNKNOWN_COURSE_LIST.set('Painting', LEVEL_CP);
|
||||||
|
|
||||||
|
export class CourseUtils
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Format course to tab index string
|
||||||
|
*
|
||||||
|
* @param course Course object
|
||||||
|
* @return string Tab index
|
||||||
|
*/
|
||||||
|
public static formatTabIndex(course: Course): string
|
||||||
|
{
|
||||||
|
return `course/${course.id}/${course.name.toLowerCase().split(' ').join('-')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect course level based on course name
|
||||||
|
*
|
||||||
|
* @param name Course name
|
||||||
|
*/
|
||||||
|
static detectLevel(name: string)
|
||||||
|
{
|
||||||
|
// Common ones
|
||||||
|
if (name.startsWith('AP')) return LEVEL_AP;
|
||||||
|
if (name.endsWith(' H')) return LEVEL_H;
|
||||||
|
if (name.endsWith(' A')) return LEVEL_A;
|
||||||
|
if (name.endsWith(' CP')) return LEVEL_CP;
|
||||||
|
if (name.startsWith('HS ')) return LEVEL_CLUB;
|
||||||
|
if (name.startsWith('MS ')) return LEVEL_CLUB;
|
||||||
|
|
||||||
|
// Uncommon ones
|
||||||
|
let lower = name.toLowerCase();
|
||||||
|
|
||||||
|
if (name.startsWith('Pre-AP')) return LEVEL_AP;
|
||||||
|
if (lower.endsWith(' acc')) return LEVEL_A;
|
||||||
|
if (name.endsWith('H')) return LEVEL_H;
|
||||||
|
if (name.endsWith('A')) return LEVEL_A;
|
||||||
|
if (name.endsWith('CP')) return LEVEL_CP;
|
||||||
|
|
||||||
|
// Even more uncommon
|
||||||
|
if (lower.includes('honors')) return LEVEL_H;
|
||||||
|
if (lower.includes('accelerated')) return LEVEL_A;
|
||||||
|
if (name.includes('Advanced')) return LEVEL_A;
|
||||||
|
|
||||||
|
// Unknown course list
|
||||||
|
if (UNKNOWN_COURSE_LIST.has(name)) return UNKNOWN_COURSE_LIST.get(name);
|
||||||
|
|
||||||
|
// Really unknown
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
export class FormatUtils
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Convert date format to yyyy-mm-dd
|
||||||
|
*
|
||||||
|
* @param _date Date
|
||||||
|
*/
|
||||||
|
public static toChartDate(_date: string | Date)
|
||||||
|
{
|
||||||
|
// Convert to Date
|
||||||
|
let date: Date = _date instanceof Date ? _date : new Date(_date);
|
||||||
|
|
||||||
|
// Convert to yyyy-mm-dd
|
||||||
|
return moment(date).format('YYYY-MM-DD');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Limit string length
|
||||||
|
*
|
||||||
|
* @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, '&');
|
||||||
|
}
|
||||||
|
}
|
||||||
+119
-33
@@ -1,31 +1,34 @@
|
|||||||
|
import Course from '@/logic/course';
|
||||||
|
import {Assignment} from '@/components/app/app';
|
||||||
|
|
||||||
|
export interface Scale
|
||||||
|
{
|
||||||
|
min: number
|
||||||
|
letter: string
|
||||||
|
gp: number
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is an utility class to calculate GPA.
|
* This is an utility class to calculate GPA.
|
||||||
*/
|
*/
|
||||||
import {Course} from '@/components/app/app';
|
|
||||||
|
|
||||||
export class GPAUtils
|
export class GPAUtils
|
||||||
{
|
{
|
||||||
// [[Min score, Letter grade, Base GPA], ...]
|
// [[Min score, Letter grade, Base GPA], ...]
|
||||||
public static SCALE =
|
public static SCALE: Scale[] =
|
||||||
[
|
[
|
||||||
[96.5, 'A+', 4.00],
|
{min: 96.5, letter: 'A+', gp: 4.00},
|
||||||
[92.5, 'A' , 3.75],
|
{min: 92.5, letter: 'A' , gp: 3.75},
|
||||||
[89.5, 'A-', 3.50],
|
{min: 89.5, letter: 'A-', gp: 3.50},
|
||||||
[86.5, 'B+', 3.25],
|
{min: 86.5, letter: 'B+', gp: 3.25},
|
||||||
[82.5, 'B' , 3.00],
|
{min: 82.5, letter: 'B' , gp: 3.00},
|
||||||
[79.5, 'B-', 2.75],
|
{min: 79.5, letter: 'B-', gp: 2.75},
|
||||||
[76.5, 'C+', 2.50],
|
{min: 76.5, letter: 'C+', gp: 2.50},
|
||||||
[72.5, 'C' , 2.25],
|
{min: 72.5, letter: 'C' , gp: 2.25},
|
||||||
[70.5, 'C-', 2.00],
|
{min: 70.5, letter: 'C-', gp: 2.00},
|
||||||
[69.5, 'D' , 1.00],
|
{min: 69.5, letter: 'D' , gp: 1.00},
|
||||||
[0 , 'F' , 0.00]
|
{min: 0 , letter: 'F' , gp: 0.00}
|
||||||
];
|
];
|
||||||
|
|
||||||
// Keywords
|
|
||||||
public static MIN = 0;
|
|
||||||
public static LETTER = 1;
|
|
||||||
public static GPA = 2;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate GPA for a list of couses
|
* Calculate GPA for a list of couses
|
||||||
*
|
*
|
||||||
@@ -81,25 +84,108 @@ export class GPAUtils
|
|||||||
* @param course Course
|
* @param course Course
|
||||||
* @param letterGrade Letter grade
|
* @param letterGrade Letter grade
|
||||||
*/
|
*/
|
||||||
public static getGP(course: Course, letterGrade?: string): number
|
public static getGP(course: Course, letterGrade: string): number
|
||||||
{
|
{
|
||||||
// Find the GPA for this course.
|
// Get scale
|
||||||
for (let scale of this.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')
|
||||||
{
|
{
|
||||||
// Letter grades are the same
|
return this.SCALE.find(scale => scale.letter == grade);
|
||||||
if (scale[this.LETTER] == letterGrade)
|
}
|
||||||
|
|
||||||
|
// 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.complete != 'Complete') return;
|
||||||
|
|
||||||
|
// Record scores
|
||||||
|
score += assignment.score;
|
||||||
|
max += assignment.scoreMax;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return
|
||||||
|
return score / max * 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the percent type
|
||||||
|
* TODO: Combine it with overall-line
|
||||||
|
*
|
||||||
|
* @param course
|
||||||
|
* @param assignments
|
||||||
|
*/
|
||||||
|
public static getPercentTypeAverage(course: Course, 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.complete != 'Complete') 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 course.grading.weightingMap)
|
||||||
|
{
|
||||||
|
if (typeScores[type] != undefined)
|
||||||
{
|
{
|
||||||
// Get grade and add it
|
totalPercentage += course.grading.weightingMap[type];
|
||||||
let grade = <number> scale[this.GPA];
|
|
||||||
|
|
||||||
// Add scaleUp if not failed.
|
|
||||||
if (grade != 0) grade += course.scaleUp;
|
|
||||||
|
|
||||||
// That's it
|
|
||||||
return grade;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return -1;
|
// Count
|
||||||
|
let score = 0;
|
||||||
|
for (let type in typeScores)
|
||||||
|
{
|
||||||
|
let typeFactor = course.grading.weightingMap[type] / totalPercentage;
|
||||||
|
score += typeScores[type] * typeFactor / typeCounts[type];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add average to the row
|
||||||
|
return score * 100;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,6 @@ export class HttpUtils
|
|||||||
{
|
{
|
||||||
public token: string = '';
|
public token: string = '';
|
||||||
|
|
||||||
constructor (token: string)
|
|
||||||
{
|
|
||||||
this.token = token;
|
|
||||||
}
|
|
||||||
|
|
||||||
public post(node: string, body: any): Promise<any>
|
public post(node: string, body: any): Promise<any>
|
||||||
{
|
{
|
||||||
// Add token
|
// Add token
|
||||||
|
|||||||
+16
-14
@@ -1,4 +1,4 @@
|
|||||||
import {Grade} from '@/components/app/app';
|
import {Assignment} from '@/components/app/app';
|
||||||
|
|
||||||
export default class JsonUtils
|
export default class JsonUtils
|
||||||
{
|
{
|
||||||
@@ -6,28 +6,30 @@ export default class JsonUtils
|
|||||||
* This method filters the information provided in an assignments json.
|
* This method filters the information provided in an assignments json.
|
||||||
*
|
*
|
||||||
* @param assignments Assignments object
|
* @param assignments Assignments object
|
||||||
* @returns Grade[] Filtered assignment grade object list
|
* @returns Assignment[] Filtered assignment grade object list
|
||||||
*/
|
*/
|
||||||
public static filterAssignments(assignments: any): Grade[]
|
public static filterAssignments(assignments: any): Assignment[]
|
||||||
{
|
{
|
||||||
let result: Grade[] = [];
|
return assignments.assignments.map((assignment: any) =>
|
||||||
|
|
||||||
assignments.assignments.forEach((assignment: any) =>
|
|
||||||
{
|
{
|
||||||
result.push(
|
return {
|
||||||
{
|
id: assignment.assignment_id,
|
||||||
|
scoreId: assignment.score_id,
|
||||||
type: assignment.assignment_type,
|
type: assignment.assignment_type,
|
||||||
|
typeId: assignment.assignment_type_id,
|
||||||
description: assignment.assignment_description,
|
description: assignment.assignment_description,
|
||||||
date: assignment._date,
|
date: new Date(assignment._date),
|
||||||
complete: assignment.completion_status,
|
complete: assignment.completion_status,
|
||||||
include: assignment.include_in_calculated_grade == 1,
|
include: assignment.include_in_calculated_grade == 1,
|
||||||
display: assignment.display_grade == 1,
|
display: assignment.display_grade == 1,
|
||||||
|
|
||||||
scoreMax: assignment.maximum_score,
|
unread: assignment.is_unread == 1,
|
||||||
score: +assignment.raw_score
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
scoreMax: assignment.maximum_score,
|
||||||
|
score: +assignment.raw_score,
|
||||||
|
|
||||||
|
gradingPeriod: +assignment.grading_period.replace('Quarter ', '') - 1
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-1
@@ -24,7 +24,10 @@
|
|||||||
"dom",
|
"dom",
|
||||||
"dom.iterable",
|
"dom.iterable",
|
||||||
"scripthost"
|
"scripthost"
|
||||||
]
|
],
|
||||||
|
|
||||||
|
// Custom
|
||||||
|
"strictPropertyInitialization": false
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src/**/*.ts",
|
"src/**/*.ts",
|
||||||
|
|||||||
Reference in New Issue
Block a user