Compare commits

...

650 Commits

Author SHA1 Message Date
Hykilpikonna 7c8bde24b3 [U] Release v0.5.4.1391 2020-03-07 16:55:27 -05:00
Hykilpikonna 158344f24f [F] Only average graded assignments in type average 2020-03-07 16:15:59 -05:00
Hykilpikonna 78159fda3f [S] Reverse alternating shading order 2020-03-07 16:13:03 -05:00
Hykilpikonna 8988e02793 [S] Fix assignment alignment issues on smaller screens 2020-03-07 16:12:24 -05:00
Hykilpikonna 6e7642e126 [S] Make min-width look better 2020-03-07 16:12:02 -05:00
Hykilpikonna 47bea6197a [S] Make bottom margin larger 2020-03-07 16:11:35 -05:00
Hykilpikonna ea17b3e9ea [S] Use alternating shading for gades 2020-03-07 16:11:23 -05:00
Hykilpikonna da719a0a6b [O] Treat close as clearUnread(false) 2020-03-07 16:10:57 -05:00
Hykilpikonna f654527311 [O] Only display grade if graded 2020-03-07 16:10:14 -05:00
Hykilpikonna 880dea1bef [F] Should display un-graded assignments too 2020-03-07 16:09:40 -05:00
Hykilpikonna 021915d9d4 [F] Include "incomplete" in GPA calculation 2020-03-07 16:08:53 -05:00
Hykilpikonna 1fb0102328 [F] NREQ shouldn't be graded 2020-03-07 16:08:19 -05:00
Hykilpikonna 1fc2a2adda [+] Support "incomplete" as a completion status 2020-03-07 16:08:15 -05:00
Hykilpikonna ddc1024611 [O] Support "pending" as a completion status 2020-03-07 16:06:56 -05:00
Hykilpikonna 1de041351c [O] Order completion status by id 2020-03-07 16:06:23 -05:00
Hykilpikonna f64cf0ad02 [F] Fix unread clearing issue 2020-03-05 18:25:42 -05:00
Hykilpikonna 46ef56efbd [M] Move deploy script 2020-01-27 16:49:48 -05:00
Hykilpikonna 7b04d1e006 [F] Enable third quarter 2020-01-27 16:49:38 -05:00
Hykilpikonna 511d4544b6 [U] Release v0.5.3.1373 2020-01-27 16:45:09 -05:00
Hykilpikonna 140893a9b2 [S] Correct margins
#3
2020-01-27 16:38:08 -05:00
Hykilpikonna 427773f4cb [+] Display assignment completion status if it is not complete
#3
2020-01-27 16:37:48 -05:00
Hykilpikonna dfeb5d0272 [+] Create method to get completion status color
#3
2020-01-27 16:37:10 -05:00
Hykilpikonna 304ba63771 [+] Create method to get completion status
#3
2020-01-27 16:36:43 -05:00
Hykilpikonna 1698f1737d [+] Include late and NREQ assignments
#3
2020-01-27 16:03:09 -05:00
Hykilpikonna 9d8a797c54 [-] Remove background for now 2020-01-23 20:35:16 -05:00
Hykilpikonna 4d0c48619d [+] Add title 2020-01-05 18:27:12 -05:00
Hykilpikonna e95d212628 [O] Compress background 2020-01-05 18:25:42 -05:00
Hykilpikonna 936e6ffa41 [+] Create info page background 2020-01-05 18:23:32 -05:00
Hykilpikonna f2a3260b8f [F] Dark mode should not apply to info page 2020-01-05 17:33:25 -05:00
Hykilpikonna 5c8b2b37d4 [O] Mark darkmode as unfinished 2020-01-05 17:31:33 -05:00
Hykilpikonna 44e4b615ba [S] Auto opacity based on darkmode 2020-01-05 17:31:18 -05:00
Hykilpikonna 60a852815b [S] Dark mode for unread number 2020-01-05 17:18:15 -05:00
Hykilpikonna 7c7d7e609a [S] Make course assignment type background look nicer 2020-01-05 17:17:43 -05:00
Hykilpikonna 0da5be9448 [S] Fix darkmode for navbar 2020-01-05 17:13:39 -05:00
Hykilpikonna 1ebb69c89c [S] Fix darkmode for desktop background 2020-01-05 17:13:29 -05:00
Hykilpikonna 99af1c4607 [S] Remove awkward white areas 2020-01-05 17:10:46 -05:00
Hykilpikonna e1caacce34 [+] Implement dark mode switcing 2020-01-05 17:09:13 -05:00
Hykilpikonna e2b6a92743 [O] Change text depending on isDark value 2020-01-05 17:09:03 -05:00
Hykilpikonna 982c8cf872 [+] Create isDark() function 2020-01-05 17:07:58 -05:00
Hykilpikonna 1e928ac1a8 [+] Add darkmode button 2020-01-05 17:07:39 -05:00
Hykilpikonna a17ab1511d [+] Add dark mode class depending on the switch 2020-01-05 17:07:28 -05:00
Hykilpikonna 07c55df766 [+] Initialize dark mode switch to cookie value 2020-01-05 17:07:15 -05:00
Hykilpikonna 06ee7b4de9 [+] Add dark mode switch 2020-01-05 17:07:00 -05:00
Hykilpikonna 0c520d5f50 [+] Use logo more compatible to dark mode 2020-01-05 17:06:48 -05:00
Hykilpikonna 522867a2dc [F] Ignore static page detection for nav-controller 2020-01-05 16:23:17 -05:00
Hykilpikonna d56d985734 [+] Display content according to staticpage 2020-01-05 16:22:56 -05:00
Hykilpikonna 1d404b4f51 [+] Detect static pages 2020-01-05 16:22:25 -05:00
Hykilpikonna c1d3cc88bc [+] Add static page field 2020-01-05 16:22:18 -05:00
Hykilpikonna 1242e2ed58 [+] Import info static page 2020-01-05 16:22:10 -05:00
Hykilpikonna f61dccb90a [+] Add hello world for testing 2020-01-04 21:15:42 -05:00
Hykilpikonna 106da7e7f1 [+] Create info component 2020-01-04 18:57:17 -05:00
Hykilpikonna 4774ad658b [O] Ignore jsons from tslint 2020-01-04 18:51:43 -05:00
Hykilpikonna 6035fe5318 [F] Fix older version url navigation issue 2020-01-04 18:51:26 -05:00
Hykilpikonna 452ac69fb6 [+] Add reminder in deploy script 2020-01-04 18:47:25 -05:00
Hykilpikonna 3ec604f049 [+] Redirect from vera.hydev.org/info 2020-01-04 18:47:13 -05:00
Hykilpikonna f5c093960a [U] Release v0.5.2.1335 2019-12-21 20:47:52 -05:00
Hykilpikonna 984fa394a5 [F] Fix history state null pointer 2019-12-21 20:47:11 -05:00
Hykilpikonna db12a5b9e7 [S] Fix updates style 2019-12-21 20:42:55 -05:00
Hykilpikonna dc57ee16c9 [U] Release v0.5.2.1332 2019-12-21 20:39:15 -05:00
Hykilpikonna 975542dab2 [S] Move the cancel button to the left 2019-12-21 20:38:41 -05:00
Hykilpikonna 5ad2be88c1 [+] Implement mark all as read 2019-12-21 20:27:23 -05:00
Hykilpikonna 76ec5d4476 [S] Make margins line up with the other cards 2019-12-21 20:24:12 -05:00
Hykilpikonna 62237d5f5f [S] Round to one digit 2019-12-21 20:23:52 -05:00
Hykilpikonna eeb2ae56d7 [S] Add margins to progress bar 2019-12-21 20:22:32 -05:00
Hykilpikonna bb1751f442 [S] Change progress bar color to green 2019-12-21 20:21:58 -05:00
Hykilpikonna a4241f9549 [S] Make progress bar visible 2019-12-21 20:21:43 -05:00
Hykilpikonna 693d479b0f [F] Progress percentage * 100 2019-12-21 20:20:35 -05:00
Hykilpikonna cf5c59fb78 [O] Separate started flag 2019-12-21 20:20:06 -05:00
Hykilpikonna 311055a4c9 [O] Create progress getter 2019-12-21 20:19:50 -05:00
Hykilpikonna 02d5e209d9 [U] Update usage in overall-course 2019-12-21 20:14:05 -05:00
Hykilpikonna e4ebaf2935 [+] Call callbacks when unread is updated 2019-12-21 20:13:53 -05:00
Hykilpikonna 97fe8395e6 [+] Encapsulate addCallback() 2019-12-21 20:13:42 -05:00
Hykilpikonna d7b319b6c9 [+] Create updateCallbacks in assignment 2019-12-21 20:13:32 -05:00
Hykilpikonna 1fdf6a8a72 [F] Fix import 2019-12-21 19:52:26 -05:00
Hykilpikonna 3425894c24 [F] Fix vue update problem 2019-12-21 19:45:08 -05:00
Hykilpikonna 3e6caab023 [S] Fix dialog checkbox alignment 2019-12-21 19:39:37 -05:00
Hykilpikonna 7e23919c0d [S] Fix word breaking globally 2019-12-21 19:39:26 -05:00
Hykilpikonna 6b7a5af7ae [O] Inline overall-course 2019-12-21 19:39:03 -05:00
Hykilpikonna 74bfec92f6 [U] Update usage in overall-course 2019-12-21 19:38:46 -05:00
Hykilpikonna 30030a0ca5 [+] Encapsulate markAsRead() 2019-12-21 19:38:28 -05:00
Hykilpikonna 78590f96f1 [O] Change that to -1 2019-12-21 19:24:23 -05:00
Hykilpikonna cc485708c5 [U] Only show progress when progress is not 0 2019-12-21 19:24:02 -05:00
Hykilpikonna daeca0fb0b [O] Use better cookie name 2019-12-21 19:23:13 -05:00
Hykilpikonna 5b5704bd75 [+] Set cookies when don't ask again is checked 2019-12-21 19:20:27 -05:00
Hykilpikonna 762be8bfba [+] Add progress bar 2019-12-21 19:10:02 -05:00
Hykilpikonna b224f07e00 [U] Update unread usage in vue 2019-12-21 18:56:12 -05:00
Hykilpikonna b1a902f92e [O] Optimize mark as unread 2019-12-21 18:55:02 -05:00
Hykilpikonna aeadb6fc8b [O] Optimize count unread for overall-course 2019-12-21 18:54:50 -05:00
Hykilpikonna e8aeecb27c [+] Add don't ask again checkbox 2019-12-21 18:40:56 -05:00
Hykilpikonna 29131227b5 [O] Use better variable names 2019-12-21 18:40:43 -05:00
Hykilpikonna 9b3a61e5cc [O] Cache unread assignments 2019-12-21 18:39:47 -05:00
Hykilpikonna 4bc9113b96 [+] Add image indicating too many unread 2019-12-21 18:32:24 -05:00
Hykilpikonna 45e0ac6066 [S] Fix word newline stuff 2019-12-21 18:26:09 -05:00
Hykilpikonna 4047a3e8f6 [+] Detect notification count 2019-12-21 18:25:43 -05:00
Hykilpikonna d4d8312016 [+] Create too many notifications prompt 2019-12-21 18:25:27 -05:00
Hykilpikonna e60df664c1 [O] Only show term grades if the selected term is "all year" 2019-12-21 17:32:03 -05:00
Hykilpikonna 916c586cbc [O] Encapsulate rawSelectedTerm() 2019-12-21 17:30:45 -05:00
Hykilpikonna a903b2cdfa [S] Largen the gap between term and current grades 2019-12-21 17:24:02 -05:00
Hykilpikonna 79f3a8e497 [S] Adjust term and numeric color to create contrast 2019-12-21 17:20:53 -05:00
Hykilpikonna 5905eb1923 [S] Adjust letter grade font size 2019-12-21 17:20:35 -05:00
Hykilpikonna 6dd3d898aa [S] Set color to gray 2019-12-21 17:20:24 -05:00
Hykilpikonna 7c86ecf3b5 [S] Apply font size to numeric too 2019-12-21 17:20:12 -05:00
Hykilpikonna 45e1376a8f [S] Auto adjust width based on contnet 2019-12-21 17:16:40 -05:00
Hykilpikonna 07f3544ba5 [S] Reduce term font size 2019-12-21 17:16:16 -05:00
Hykilpikonna 4f93539673 [F] Fix: displaying zero based counting 2019-12-21 17:14:09 -05:00
Hykilpikonna 095dfdb54f [+] Display term number 2019-12-21 17:13:48 -05:00
Hykilpikonna eaac2b9332 [S] Vertical align letter and numeric 2019-12-21 17:13:38 -05:00
Hykilpikonna e63547140b [S] Fix reversed array 2019-12-21 17:08:32 -05:00
Hykilpikonna 99aa7b4093 [S] Fix display order 2019-12-21 17:08:02 -05:00
Hykilpikonna b4e6dcdb8c [S] Adjust alignment 2019-12-21 17:07:51 -05:00
Hykilpikonna 2d182c3cf4 [+] Display term grades 2019-12-21 17:02:35 -05:00
Hykilpikonna 05feb8c9f3 [O] Enable caching for numeric grade 2019-12-21 17:02:08 -05:00
Hykilpikonna 3adfdb5aac [+] Encapsulate course.letterGradeTerm() 2019-12-21 17:01:57 -05:00
Hykilpikonna c0ec2e03ff [+] Encapsulate getAllGradingPeriods() 2019-12-21 17:01:37 -05:00
Hykilpikonna e128704552 [S] Optimize other selectors 2019-12-21 15:18:50 -05:00
Hykilpikonna af2aba9a50 [S] Optimize css selectors for block-info and block-grade 2019-12-21 15:16:10 -05:00
Hykilpikonna dbdd33d050 [S] Scope css 2019-12-21 15:13:13 -05:00
Hykilpikonna d230636a0e [O] Remove el-row from course-head 2019-12-21 15:13:04 -05:00
Hykilpikonna e7d639af33 [O] Optimize course-head clickable 2019-12-21 14:50:17 -05:00
Hykilpikonna b97ca4f699 [F] Fix history refresh problem 2019-12-21 14:46:06 -05:00
Hykilpikonna 1657330c57 [-] Remove installed element-ui js library 2019-12-21 14:45:14 -05:00
Hykilpikonna dd8bbf2cbe [U] Update dependencies 2019-12-21 14:44:37 -05:00
Hykilpikonna 53065c9dd3 [O] Serialize index 2019-12-21 14:09:14 -05:00
Hykilpikonna 27bcd6b9b6 [F] Fix navigator selection 2019-12-21 14:08:55 -05:00
Hykilpikonna bc190cbdfb [F] Fix problem where history activates twice 2019-12-21 13:53:02 -05:00
Hykilpikonna 755b384b76 [F] Fix initial index conversion 2019-12-21 13:41:53 -05:00
Hykilpikonna a3f03f5577 [O] Optimize null case with param default 2019-12-21 13:40:46 -05:00
Hykilpikonna 57f29262e3 [O] Separate checkIndex 2019-12-21 13:40:34 -05:00
Hykilpikonna ac5549ccc4 [F] Remove lastTab from history state 2019-12-21 13:40:05 -05:00
Hykilpikonna d3ba0af4f5 [F] Update current index 2019-12-21 13:32:32 -05:00
Hykilpikonna 6312515b7b [U] Update nav usage in navigation vue 2019-12-21 13:31:58 -05:00
Hykilpikonna 41cdbe93b1 [F] Fix error due to auto caching of get methods 2019-12-21 13:31:45 -05:00
Hykilpikonna 702eb4d8f3 [U] Update usage 2019-12-21 13:25:10 -05:00
Hykilpikonna 7581ea016e [O] Specify type for id and info 2019-12-21 13:24:22 -05:00
Hykilpikonna 4938b2a72a [O] Make identifier required 2019-12-21 13:23:21 -05:00
Hykilpikonna 815258e8be [+] Add identifier and info in course.urlIndex 2019-12-21 13:21:37 -05:00
Hykilpikonna adb211c58a [+] Add info to index 2019-12-21 13:20:32 -05:00
Hykilpikonna cdeae1e1d4 [+] Add identifier to index 2019-12-21 13:20:18 -05:00
Hykilpikonna dfbe255191 [M] Shorten var name "activeIndex" to index 2019-12-21 13:20:08 -05:00
Hykilpikonna f8f1c1f8a3 [F] Fix course-head navigator usage 2019-12-21 13:11:35 -05:00
Hykilpikonna 4e5809a1dc [U] Update app component usage 2019-12-21 13:08:41 -05:00
Hykilpikonna aef5fa47fc [U] Update usage in Navigation 2019-12-21 13:07:42 -05:00
Hykilpikonna ca751c27d1 [O] Remove unnecessary getTitle() 2019-12-21 12:59:42 -05:00
Hykilpikonna 95690fe046 [O] Specify type 2019-12-21 12:58:02 -05:00
Hykilpikonna 1fd17706e4 [-] Remove duplicate in course-utils 2019-12-21 12:56:23 -05:00
Hykilpikonna 082400abe8 [+] Encapsulate get urlIndex in course 2019-12-21 12:56:13 -05:00
Hykilpikonna e90741b6bc [+] Encapsulate get urlHash in Course 2019-12-21 12:55:59 -05:00
Hykilpikonna 71352ee39a [O] Make activeIndex accessable 2019-12-21 12:50:02 -05:00
Hykilpikonna caa6b38673 [M] Rename Navigator class to NavController to avoid conflict 2019-12-21 12:47:10 -05:00
Hykilpikonna 49c23c9562 [+] Create navigator field in app 2019-12-21 12:40:56 -05:00
Hykilpikonna 94424ea288 [M] Rename filteredCourses to gradedCourses 2019-12-21 12:40:43 -05:00
Hykilpikonna 956119be2a [O] Optimize assignmentsReady 2019-12-21 12:39:48 -05:00
Hykilpikonna e26decd77d [O] Remove unnecessary public 2019-12-21 12:32:36 -05:00
Hykilpikonna 4d7b41bf2a [+] Create constructor 2019-12-21 11:52:16 -05:00
Hykilpikonna e6cfa30ed4 [O] Optimize encapsulation 2019-12-21 11:51:12 -05:00
Hykilpikonna ea46d16836 [+] Encapsulate updateIndex with only hash 2019-12-21 11:49:06 -05:00
Hykilpikonna 128dbb2ca7 [+] Auto format title if null 2019-12-21 11:44:43 -05:00
Hykilpikonna 097985f087 [O] Optimize nullable format 2019-12-21 11:44:21 -05:00
Hykilpikonna 26fa50ead2 [O] Make title nullable 2019-12-21 11:43:26 -05:00
Hykilpikonna 14a9c5a9b2 [O] Combine index and title 2019-12-21 11:37:25 -05:00
Hykilpikonna 55432e5c33 [+] Create navigator class 2019-12-21 11:34:09 -05:00
Hykilpikonna a2f6a30ad1 [U] Update packages 2019-12-08 13:05:26 -05:00
Hykilpikonna bee2001013 [U] Release v0.5.1.1229 2019-12-07 20:22:36 -05:00
Hykilpikonna facb76c568 [O] Divide nickname with signout 2019-12-07 20:19:51 -05:00
Hykilpikonna aaa36382bc [+] Show nickname on dropdown 2019-12-07 20:19:38 -05:00
Hykilpikonna f66f3551fd [+] Add command to dropdown menu 2019-12-07 17:12:08 -05:00
Hykilpikonna 2671d9a5ba [+] Create avatar menu event function 2019-12-07 17:11:53 -05:00
Hykilpikonna 43174bf294 [-] Remove unnecessary public 2019-12-07 17:08:19 -05:00
Hykilpikonna d69b74571a [+] Add icon to sign-out 2019-12-07 17:06:00 -05:00
Hykilpikonna 1976805f60 [S] Set dropdown font 2019-12-07 16:58:03 -05:00
Hykilpikonna d2af24a574 [S] Optimize font variable 2019-12-07 16:57:56 -05:00
Hykilpikonna a7bb39bc76 [S] Import Nunito font 2019-12-07 16:57:37 -05:00
Hykilpikonna 915fcb94dc [-] Remove signout button 2019-12-07 16:40:40 -05:00
Hykilpikonna eb8ac2b7a5 [O] Trigger on click 2019-12-07 16:38:36 -05:00
Hykilpikonna 282c2aa1b2 [+] Add dropdown to avatar 2019-12-07 16:38:13 -05:00
Hykilpikonna eed72aaa28 [F] Fix potential NaN 2019-12-07 16:03:13 -05:00
Hykilpikonna 0650af48af [F] Use LetterGrade for more accuracy 2019-12-07 15:59:42 -05:00
Hykilpikonna 11ccd9f28a [F] Don't include NaN in GPA calculation 2019-12-07 15:59:27 -05:00
Hykilpikonna ef44e4d149 [F] Account for all year situation 2019-12-07 15:07:47 -05:00
Hykilpikonna 4a220ddc60 [+] Encapsulate getStartDate() 2019-12-07 15:05:46 -05:00
Hykilpikonna 0e60a3d93d [+] Switch terms when generating series data 2019-12-07 15:05:30 -05:00
Hykilpikonna b0a6c5fece [O] Optimize code style 2019-12-07 15:05:13 -05:00
Hykilpikonna b3dfeb2add [M] Rename to shorten getSelectGradingPEriod 2019-12-07 15:04:44 -05:00
Hykilpikonna 844eeefb4c [O] Optimize in-between dates storing 2019-12-07 14:39:12 -05:00
Hykilpikonna 6d3e145558 [O] Use course.getAssignmentsBefore() 2019-12-07 14:29:46 -05:00
Hykilpikonna 25eaf9abe9 [F] Fix unable to find minDate 2019-12-07 14:29:05 -05:00
Hykilpikonna b505365762 [-] Remove duplicate sorting 2019-12-07 14:28:42 -05:00
Hykilpikonna 8402fa7dd9 [F] Fix term ending time calculation 2019-12-07 14:28:00 -05:00
Hykilpikonna 5098b696e3 [F] Sort assignments by time 2019-12-07 14:27:41 -05:00
Hykilpikonna 0487e72c1f [O] Sort by date 2019-12-07 14:16:28 -05:00
Hykilpikonna 40ba71e1d8 [F] Fix date duplicate problem 2019-12-07 14:15:59 -05:00
Hykilpikonna 643430553b [-] Remove unused variable i 2019-12-07 14:15:43 -05:00
Hykilpikonna 940bf3635c [F] Only return assignments of this grading period 2019-12-07 14:12:33 -05:00
Hykilpikonna 2d119ce531 [F] Fix not found problem 2019-12-07 14:05:09 -05:00
Hykilpikonna fe3d96ec9c [F] Fix index problem when finding last index 2019-12-07 14:04:23 -05:00
Hykilpikonna bf4f2f20cf [O] Use timestamp instead of date 2019-12-07 13:56:17 -05:00
Hykilpikonna 6c8e888807 [+] Encapsulate getAssignmentsBefore(date) 2019-12-07 13:51:49 -05:00
Hykilpikonna d71fb87a21 [S] Adjust avatar positioning 2019-12-07 13:51:24 -05:00
Hykilpikonna 6695cad008 [U] Update pWaitFor callback 2019-12-07 13:51:01 -05:00
Hykilpikonna 1e6e21200a [O] Fill new array with null 2019-12-07 13:49:50 -05:00
Hykilpikonna 32313461c6 [U] Remove unnecessary public 2019-12-07 13:48:33 -05:00
Hykilpikonna 34c562c8e9 [F] Force cookie update 2019-12-07 13:48:09 -05:00
Hykilpikonna ec4f66f4f3 [U] Use getTerm 2019-12-07 13:47:56 -05:00
Hykilpikonna 3e37ab15fc [+] Encapsulate getTerm(date) 2019-12-07 13:47:35 -05:00
Hykilpikonna 16e7e408bc [O] Force update cookies 2019-12-07 12:34:11 -05:00
Hykilpikonna 016a8a3960 [U] Update termGrading usage 2019-12-07 12:33:54 -05:00
Hykilpikonna 022c7ec50f [U] Update usage 2019-12-07 12:32:57 -05:00
Hykilpikonna bac9e3524c [O] Use encapsulated graded() method 2019-12-07 12:32:46 -05:00
Hykilpikonna e26d46a5d2 [U] Pass in grading when calculating PT average 2019-12-07 12:32:32 -05:00
Hykilpikonna 314828a089 [+] Encapsulate get numericGrade 2019-12-07 12:26:05 -05:00
Hykilpikonna 4090ec125a [O] Cache grading periods 2019-12-07 12:25:37 -05:00
Hykilpikonna 6f2c5feef6 [O] Only return valid terms 2019-12-07 12:24:33 -05:00
Hykilpikonna 2fcb5348c9 [U] Update usage 2019-12-07 11:59:49 -05:00
Hykilpikonna 964fee639d [-] Remove unused allYearGrade field 2019-12-07 11:58:53 -05:00
Hykilpikonna b21a76e3c6 [O] Put term assignments outside of computed 2019-12-07 11:58:36 -05:00
Hykilpikonna 344f2628e3 [-] Remove one-line encapsulation 2019-12-07 11:57:09 -05:00
Hykilpikonna fdb4752e87 [U] Load raw assignments 2019-12-07 11:55:02 -05:00
Hykilpikonna 9afd71a06c [-] Remove unnecessary JsonUtils 2019-12-07 11:54:45 -05:00
Hykilpikonna 6e25bd6b17 [-] Remove unused variable declaration 2019-12-07 11:53:15 -05:00
Hykilpikonna fd01dda8b3 [+] Encapsulate getGraded() in assignment 2019-12-07 11:53:05 -05:00
Hykilpikonna 24ab0f66f3 [+] Create constructor for assignments 2019-12-07 11:52:34 -05:00
Hykilpikonna 4a51e8ecdb [O] Make Assignment interface a class 2019-12-07 11:52:19 -05:00
Hykilpikonna 32ccac2d15 [O] Find current term dynamically 2019-12-07 11:42:40 -05:00
Hykilpikonna 8ba8489594 [+] Encapsulate findLastIndex() 2019-12-07 11:37:56 -05:00
Hykilpikonna bf4479490f [U] Get numeric grade by term 2019-12-07 11:25:39 -05:00
Hykilpikonna 8846a8be5d [O] Get assignments return all selected terms assignments 2019-12-07 11:25:23 -05:00
Hykilpikonna 99f1a4c58a [+] Encapsulate getGradingPeriods() 2019-12-07 11:21:19 -05:00
Hykilpikonna e57b5a5d69 [+] Encapsulate getGrading(term) 2019-12-07 11:17:30 -05:00
Hykilpikonna e44724a173 [O] Separate grading class 2019-12-07 11:17:17 -05:00
Hykilpikonna 1513a3016c [F] Load grading for each term 2019-12-06 21:22:40 -05:00
Hykilpikonna 81c79d3867 [+] Initialize grading as empty array 2019-12-06 21:22:17 -05:00
Hykilpikonna 8c3f398753 [F] Make weightingMap an array 2019-12-06 21:22:01 -05:00
Hykilpikonna 2a2fae8a97 Revert "[O] Make cache() a function"
This reverts commit bf9d90d80f.
2019-12-06 18:18:41 -05:00
Hykilpikonna 3c632c0488 Revert "[U] Update usage"
This reverts commit dc3365b594.
2019-12-06 18:18:38 -05:00
Hykilpikonna d0519bbab4 Revert "[F] Fix logic typo"
This reverts commit f399fee29c.
2019-12-06 18:18:35 -05:00
Hykilpikonna f399fee29c [F] Fix logic typo 2019-12-06 18:13:20 -05:00
Hykilpikonna dc3365b594 [U] Update usage 2019-12-06 18:11:39 -05:00
Hykilpikonna bf9d90d80f [O] Make cache() a function 2019-12-06 18:10:50 -05:00
Hykilpikonna 2ce8d04cbd [U] Cache assignmentTypes 2019-12-06 18:08:06 -05:00
Hykilpikonna 5d2ef0fadf [U] Cache numericGrade 2019-12-06 18:07:55 -05:00
Hykilpikonna 7307b179f0 [U] Cache letterGrade() 2019-12-06 18:07:46 -05:00
Hykilpikonna 55c6119853 [U] Cache assignments() 2019-12-06 18:07:35 -05:00
Hykilpikonna aba98783db [+] Create cache field in course 2019-12-06 18:07:22 -05:00
Hykilpikonna eb1da15c8f [M] Rename Cache to CacheUtils to avoid naming conflicts 2019-12-05 23:22:43 -05:00
Hykilpikonna 698db97eb1 [+] Create get() method for cache 2019-12-05 23:19:19 -05:00
Hykilpikonna 5a5f2349ea [+] Create a cache map field 2019-12-05 23:19:02 -05:00
Hykilpikonna 86fa51496f [+] Create CacheUtils 2019-12-05 23:18:46 -05:00
Hykilpikonna bf0e1986d4 [S] Add margins to avatar 2019-12-02 22:33:19 -05:00
Hykilpikonna 20d54611b5 [S] Align avatar to the right 2019-12-02 22:33:11 -05:00
Hykilpikonna 68d37f1dc2 [+] Add class to avatar 2019-12-02 22:32:43 -05:00
Hykilpikonna 819610f4f1 [F] Multiple links didn't work 2019-12-02 22:30:50 -05:00
Hykilpikonna 97b1f5c486 [+] Add avatar 2019-12-02 22:30:34 -05:00
Hykilpikonna f39cb9e1cd [F] Only display navigation when user signed in 2019-12-02 22:30:10 -05:00
Hykilpikonna 127ec9df4a [F] Fix not initialized error 2019-12-02 22:29:05 -05:00
Hykilpikonna 4a74df1dea [U] Pass in user in app.vue 2019-12-02 22:09:43 -05:00
Hykilpikonna 27c7d3b192 [+] Add user field in navigation 2019-12-02 22:09:23 -05:00
Hykilpikonna 8a207f7b81 [+] Use all emails 2019-12-02 22:05:40 -05:00
Hykilpikonna 7f42eabbd1 [+] Generate avatar url 2019-12-02 21:59:45 -05:00
Hykilpikonna 3aef7c1223 [O] Trim emails 2019-12-02 21:59:29 -05:00
Hykilpikonna 78faa7430a [+] Add maintenance message 2019-12-02 21:59:17 -05:00
Hykilpikonna c059986a50 [+] Import md5 dependency 2019-12-02 21:50:35 -05:00
Hykilpikonna 03f7b1b56b [F] Fix maintenance page alignment 2019-12-02 17:18:58 -05:00
Hykilpikonna aead3a0021 [+] Show maintenance when it is detected 2019-12-02 17:07:42 -05:00
Hykilpikonna c908e6f157 [+] Add maintenance component 2019-12-02 17:07:01 -05:00
Hykilpikonna 26ae33f807 [+] Add message property in maintenance 2019-12-02 17:06:40 -05:00
Hykilpikonna c4b27a420f [+] Clear cookies when login expires 2019-12-02 16:46:53 -05:00
Hykilpikonna cf28f8617d [O] Remove console logging before clearing cookies 2019-12-02 16:46:40 -05:00
Hykilpikonna ca54abe788 [+] Encapsulate clearCookies() 2019-12-02 16:46:13 -05:00
Hykilpikonna ee7045a120 [+] Create maintenance page 2019-12-02 16:18:14 -05:00
Hykilpikonna d267867381 [O] Optimize imports 2019-12-02 14:51:41 -05:00
Hykilpikonna e4acf55656 [O] Remove empty component tags 2019-12-02 14:49:53 -05:00
Hykilpikonna 2cacc2175a [O] Collapse empty tags 2019-12-02 14:49:17 -05:00
Hykilpikonna 50f9e3aeea [O] Optimize calls with get functions 2019-12-02 14:43:55 -05:00
Hykilpikonna 146323a351 [M] Move loading.vue to /overlays/ 2019-12-02 14:40:40 -05:00
Hykilpikonna b0d95991f1 [+] Disable input when logging in with token 2019-12-02 14:39:15 -05:00
Hykilpikonna 987c9e462a [+] Add switch to disable input 2019-12-02 14:38:59 -05:00
Hykilpikonna c19bb5bfe0 [O] Fix a grammar mistake in constants.ts 2019-12-02 14:22:01 -05:00
Hykilpikonna e10ed6e41b [U] Update user type and usage in Http-utils.js 2019-12-02 14:21:41 -05:00
Hykilpikonna c01817cd97 [U] Update user type in App.ts 2019-12-02 14:21:24 -05:00
Hykilpikonna a3e6c8d635 [U] Update event name in app.vue 2019-12-02 14:20:59 -05:00
Hykilpikonna 6c71bb4ab2 [O] Make login() return LoginUser 2019-12-02 14:20:48 -05:00
Hykilpikonna 991580473d [+] Create constructor for it 2019-12-02 14:17:04 -05:00
Hykilpikonna c6ca171101 [+] Create login-user class 2019-12-02 14:16:54 -05:00
Hykilpikonna 520fbbec5f [O] Fix grammar mistakes in app.ts 2019-12-02 14:07:00 -05:00
Hykilpikonna 4521832a76 [+] Login with token every time 2019-12-02 14:03:35 -05:00
Hykilpikonna 7f07c7d3d6 [O] Show errors properly 2019-12-02 14:01:43 -05:00
Hykilpikonna 63891a8385 [U] Update return data usage 2019-12-02 13:59:22 -05:00
Hykilpikonna 1c52e25e9e [O] Remove unnecessary param after @component 2019-12-02 13:58:48 -05:00
Hykilpikonna af38e4b7d7 [O] Make login.ts code a lot shorter 2019-12-02 13:58:17 -05:00
Hykilpikonna e99e6170b8 [O] Encapsulate login() 2019-12-02 13:55:58 -05:00
Hykilpikonna 7550a99166 [O] Fix grammar mistakes 2019-12-02 13:55:35 -05:00
Hykilpikonna 793c4dcf16 [S] Make version start with v 2019-11-14 22:12:28 -05:00
Hykilpikonna d5bc6b0af8 [S] Correctly align version 2019-11-14 22:12:14 -05:00
Hykilpikonna 80b2980144 [S] Change the font size for version 2019-11-14 22:12:05 -05:00
Hykilpikonna 7ebcb952a9 [S] Lighten the version text 2019-11-14 22:08:42 -05:00
Hykilpikonna 99ec72ce98 [+] Add version number to navbar 2019-11-14 22:07:29 -05:00
Hykilpikonna 3b6bf4a5eb [F] Remove tooltips for pie and radar 2019-11-14 17:37:33 -05:00
Hykilpikonna e11bf75f20 [F] Fix scatter plot tooltip color 2019-11-14 17:29:29 -05:00
Hykilpikonna af56ce497f Revert "[O] Second quarter start at 0"
This reverts commit 2f90f54cb3.
2019-11-14 17:23:14 -05:00
Hykilpikonna 2f90f54cb3 [O] Second quarter start at 0 2019-11-14 17:23:09 -05:00
Hykilpikonna a94701a335 [F] Skip http request if already exists in cookies 2019-11-12 17:29:34 -05:00
Hykilpikonna 67d1525d98 [O] Optimize grading methods by storing them in cookies 2019-11-12 17:28:33 -05:00
Hykilpikonna cdf5475c3b [F] Fix weight errors when the calculated grade equals the supposed grade 2019-11-12 17:23:14 -05:00
Hykilpikonna c97a202bd5 [U] Release v0.4.6.1087 2019-11-11 22:42:42 -05:00
Hykilpikonna b11332953c [F] Fix dark theme logo displaying issue 2019-11-11 22:41:26 -05:00
Hykilpikonna d5f8649b80 [S] Adjust logo font weight 2019-11-11 22:32:01 -05:00
Hykilpikonna fab173afa3 [S] Adjust logo font size 2019-11-11 22:31:48 -05:00
Hykilpikonna 41fbf70428 [S] Create a gradient for logo 2019-11-11 22:30:41 -05:00
Hykilpikonna 1df427b4b4 [S] Add right margin to icon 2019-11-11 22:15:22 -05:00
Hykilpikonna 96a0d3bef6 [S] Remove old logo style 2019-11-11 22:15:13 -05:00
Hykilpikonna 2d917d7858 [S] Limit logo height 2019-11-11 22:15:03 -05:00
Hykilpikonna 2e851e5541 [S] Add logo in nav bar 2019-11-11 22:14:44 -05:00
Hykilpikonna b5ff93bf58 [+] Import Favicon into index.html 2019-11-11 22:09:02 -05:00
Hykilpikonna a10350756f [+] Generate 32px favicon 2019-11-11 22:08:43 -05:00
Hykilpikonna 133765131c [O] Limit logo image width 2019-11-11 21:57:18 -05:00
Hykilpikonna 723a72c1ab [+] Copy logo to assets 2019-11-11 21:53:48 -05:00
Hykilpikonna 6567aeda7d [+] Export logo as png 2019-11-11 21:53:00 -05:00
Hykilpikonna 2182ff8e74 [M] Move vue logo to icon making dir 2019-11-11 21:52:41 -05:00
Hykilpikonna 25865fb372 [+] Remake logo again 2019-11-11 21:52:21 -05:00
Hykilpikonna 81ffeb8d85 [+] Remake logo 2019-11-11 21:51:40 -05:00
Hykilpikonna 19203f3629 [+] Support email login 2019-11-10 17:14:43 -05:00
Hykilpikonna 82ad8f026b [O] Optimize field types 2019-11-10 17:14:33 -05:00
Hykilpikonna 38629a9e34 [O] Specify username format 2019-11-10 17:10:38 -05:00
Hykilpikonna 4499232544 [S] Remove card paddings 2019-11-10 16:39:33 -05:00
Hykilpikonna 960f30295c [S] Specify background color is transparent 2019-11-10 16:39:21 -05:00
Hykilpikonna 415373559c [S] Darken colors 2019-11-10 16:38:17 -05:00
Hykilpikonna a7d22e1620 [S] Add drop shadow to tooltip 2019-11-10 14:27:59 -05:00
Hykilpikonna f88ef7284f [-] Disable dark mode by default 2019-11-10 14:22:18 -05:00
Hykilpikonna 380023668f [S] Apply dark background to course page too 2019-11-10 14:21:41 -05:00
Hykilpikonna fb90900045 [S] Make graph white 2019-11-10 14:15:08 -05:00
Hykilpikonna ab511a706a [S] Compact css entries 2019-11-10 14:14:14 -05:00
Hykilpikonna 407a2889d1 [S] Use css variables 2019-11-10 14:13:38 -05:00
Hykilpikonna 840775314a [S] Fix prev-course and next-course shadow 2019-11-10 14:06:40 -05:00
Hykilpikonna 85c1d93608 [S] Apply darker background to #app-content too 2019-11-10 14:05:23 -05:00
Hykilpikonna 8825886c5c [S] Emphesize course name 2019-11-10 14:04:25 -05:00
Hykilpikonna e924c00f0d [S] Lighten expansion color 2019-11-10 14:00:32 -05:00
Hykilpikonna 03826d108d [S] Add layer effect to course card expansion 2019-11-10 13:59:38 -05:00
Hykilpikonna 1e04b4ad70 [S] Apply light text to buttons too 2019-11-10 13:59:13 -05:00
Hykilpikonna 3ac37d980c [S] Emphesize percent entry box 2019-11-10 13:54:21 -05:00
Hykilpikonna da8cec72b4 [S] Emphesize max entry box 2019-11-10 13:54:10 -05:00
Hykilpikonna 903345b86a [S] Comment overall css 2019-11-10 13:51:24 -05:00
Hykilpikonna 9ed297431d [S] Apply dark background to entry box 2019-11-10 13:50:56 -05:00
Hykilpikonna e763a1ac88 [S] Adjust backtop box shadow 2019-11-10 13:47:50 -05:00
Hykilpikonna 5b3bdc2f1f [S] Apply light text to span too 2019-11-10 13:46:10 -05:00
Hykilpikonna 65b4a68f7e [S] Remove el-card border 2019-11-10 13:44:08 -05:00
Hykilpikonna 7ff885e28b [S] Make overall background darker 2019-11-10 13:44:01 -05:00
Hykilpikonna b936f8a0ca [S] Default use light background for foreground 2019-11-10 13:40:37 -05:00
Hykilpikonna 93a01aeb5d [S] Fix padding under main frame 2019-11-10 13:35:48 -05:00
Hykilpikonna 0326c0bf14 [S] Use gray color with dark background 2019-11-10 13:34:51 -05:00
Hykilpikonna cb91a20844 [S] Apply dark background to all div 2019-11-10 13:34:39 -05:00
Hykilpikonna 9fdb7461ba [S] Create dark css 2019-11-10 13:28:47 -05:00
Hykilpikonna 15b51020fc [O] Make cards clickable 2019-11-10 13:22:26 -05:00
Hykilpikonna 2938621b73 [O] Scroll to top on page change 2019-11-10 13:22:15 -05:00
Hykilpikonna 624158f3d4 [O] Show prev-course and next-course above others 2019-11-10 13:22:02 -05:00
Hykilpikonna 602a8b9c46 [+] Add back-to-top button 2019-11-10 13:21:35 -05:00
Hykilpikonna 62338358d1 [S] Add radial color gradient to the scatter plot points 2019-11-10 13:12:22 -05:00
Hykilpikonna 9bc69a81af [+] Show label on the top 2019-11-10 13:11:51 -05:00
Hykilpikonna c93b1e4eec [+] Resize scatter plot circles according to their weight 2019-11-10 13:09:18 -05:00
Hykilpikonna d30728e2e9 [+] Add assignmentCount to AssignmentType interface 2019-11-10 13:08:22 -05:00
Hykilpikonna 7d86b7a2b9 [+] Import chroma-js instead 2019-11-10 13:02:18 -05:00
Hykilpikonna bb31e18ad5 [+] Add chroma-js dependency 2019-11-10 12:37:52 -05:00
Hykilpikonna 73dc5f6a51 [O] Optimize course-scatter with course.assignmentTypes 2019-11-10 12:05:53 -05:00
Hykilpikonna 6343a10fbc [O] Dynamic radar min value 2019-11-09 23:47:30 -05:00
Hykilpikonna 14beb802b0 [+] Display weight percentage for pie too 2019-11-09 23:29:11 -05:00
Hykilpikonna be71c4ea0b [O] Use the same text style for pie graph 2019-11-09 23:28:46 -05:00
Hykilpikonna 4bd34d46be [O] Separate textStyle to GraphUtils 2019-11-09 23:27:44 -05:00
Hykilpikonna 670ac48516 [O] Use more descriptive title for radar too 2019-11-09 23:23:52 -05:00
Hykilpikonna 4ce7c625ea [O] Use more descriptive title for pie graph 2019-11-09 23:20:22 -05:00
Hykilpikonna 551930f47a [O] Make the center empty 2019-11-09 23:08:05 -05:00
Hykilpikonna 04ca753466 [S] Adjust opacity of the pie areas 2019-11-09 22:58:06 -05:00
Hykilpikonna 4169d5235a [S] Add drop shadow 2019-11-09 22:57:03 -05:00
Hykilpikonna c417698bf2 [S] Adjust y position of pie graph 2019-11-09 22:53:22 -05:00
Hykilpikonna b6bf6373f2 [S] Reduce radius 2019-11-09 22:53:10 -05:00
Hykilpikonna f4326cf9e1 [O] Better label alignment 2019-11-09 22:53:01 -05:00
Hykilpikonna cca9d5a240 [O] Adjust to correct color after sorting 2019-11-09 22:52:52 -05:00
Hykilpikonna 0af3cee18b [O] Sort pie chart data 2019-11-09 22:51:06 -05:00
Hykilpikonna 8d122f0100 [+] Show type name 2019-11-09 22:45:55 -05:00
Hykilpikonna 62373485f5 [O] Use percentage as weight 2019-11-09 22:45:40 -05:00
Hykilpikonna 472d83e9fb [S] Adjust height 2019-11-09 22:44:22 -05:00
Hykilpikonna 34231dc480 [+] Generate pie data 2019-11-09 22:38:36 -05:00
Hykilpikonna 3f6c9c1204 [O] Round weight to 2 digits 2019-11-09 22:38:22 -05:00
Hykilpikonna d0e627bd83 [O] Change title 2019-11-09 22:33:56 -05:00
Hykilpikonna 6e4074b050 [+] Add a type-pie to course-page 2019-11-09 22:30:45 -05:00
Hykilpikonna 963df01b6f [+] Create pie graph component 2019-11-09 22:29:19 -05:00
Hykilpikonna c807d4aecb [-] Remove unused css section 2019-11-09 22:28:40 -05:00
Hykilpikonna e2c9d05a7e [M] Rename type-radar directory too 2019-11-09 22:26:13 -05:00
Hykilpikonna 6204efd453 [U] Release v0.4.3.1006 2019-11-09 22:15:31 -05:00
Hykilpikonna db083732b0 [M] Rename course-type-radar to type-radar 2019-11-09 22:14:41 -05:00
Hykilpikonna 4974049c0b [S] Make background lighter 2019-11-09 22:11:02 -05:00
Hykilpikonna 857192ee6d [S] Add text shadows 2019-11-09 22:10:45 -05:00
Hykilpikonna 90d07b1faa [S] Make font size larger 2019-11-09 22:10:33 -05:00
Hykilpikonna 4abe02da94 [+] Color the indicator texts 2019-11-09 22:10:16 -05:00
Hykilpikonna adca4b41e2 [+] Add percent score after indicator 2019-11-09 22:10:08 -05:00
Hykilpikonna 4d9d8e0be5 [S] Darken line color 2019-11-09 21:58:39 -05:00
Hykilpikonna 546ad81f7c [+] Colorize split area 2019-11-09 21:58:23 -05:00
Hykilpikonna 1eff27ad26 [S] Center color gradient 2019-11-09 21:49:40 -05:00
Hykilpikonna f540e03a56 [S] Adjust the graph down 10% 2019-11-09 21:47:34 -05:00
Hykilpikonna af925741b4 [S] Make graph smaller 2019-11-09 21:47:18 -05:00
Hykilpikonna 08bb24cac4 [O] Use percentage score instead of score 2019-11-09 21:47:06 -05:00
Hykilpikonna 8a9ca83e68 [O] Optimize graph with baseSettings 2019-11-09 21:46:47 -05:00
Hykilpikonna e6b6a73f1f [O] Optimize type adverage with pre-calculated percent 2019-11-09 21:35:18 -05:00
Hykilpikonna ceca351b07 [+] Include percent score in AssignmentType 2019-11-09 21:34:28 -05:00
Hykilpikonna 4e14730db6 [S] Change the graph symbol to circle 2019-11-09 21:24:13 -05:00
Hykilpikonna 3b0e291df4 [S] Make radar only take half the screen 2019-11-09 21:23:54 -05:00
Hykilpikonna 6bcb2577f7 [+] Add area gradient 2019-11-09 21:23:34 -05:00
Hykilpikonna 54e54b89e9 [+] Display tooltip 2019-11-09 21:23:14 -05:00
Hykilpikonna 0085d384fd [+] Display data in radar graph 2019-11-09 21:23:07 -05:00
Hykilpikonna 3b4e40261f [O] Use scoreMax instead 2019-11-09 21:22:31 -05:00
Hykilpikonna eb8715867e [+] Add radar indicator 2019-11-09 21:03:12 -05:00
Hykilpikonna 1bcfdf5648 [+] Add radar component in course page 2019-11-09 20:55:56 -05:00
Hykilpikonna 429de6553b [+] Create course-type-radar 2019-11-09 20:55:39 -05:00
Hykilpikonna fce47648c8 [O] Remove unnecessary getAssignmentTypes() 2019-11-09 20:45:30 -05:00
Hykilpikonna f6d44dd1f2 [O] Remove unnecessary complete verification 2019-11-09 20:45:12 -05:00
Hykilpikonna 4fe0a54277 [O] Remove unnecessary average() method 2019-11-09 20:44:57 -05:00
Hykilpikonna e8bf21e60c [F] Fix type name display 2019-11-09 20:44:37 -05:00
Hykilpikonna 472df39ac8 [O] Pass in AssignmentType object for AssignmentTypeHead component 2019-11-09 20:44:23 -05:00
Hykilpikonna ae94f54a7b [O] Cache assignment types 2019-11-09 20:39:24 -05:00
Hykilpikonna 8c9f0a0e83 [O] Optimize caching 2019-11-09 20:37:56 -05:00
Hykilpikonna cb22baf120 [+] Calculate type score and weight 2019-11-09 20:33:32 -05:00
Hykilpikonna 0525aae98a [+] Create method to get assignment types 2019-11-09 20:25:18 -05:00
Hykilpikonna 4c87cb2947 [F] Fix null pointer 2019-11-09 20:21:57 -05:00
Hykilpikonna 076a1ee52e [O] Return graded assignments by default 2019-11-09 20:21:09 -05:00
Hykilpikonna 7bd9e97396 [S] Remove ugly circle between data points 2019-11-09 20:05:52 -05:00
Hykilpikonna aa014bcdab [+] Create AssignmentType interface 2019-11-09 19:47:56 -05:00
Hykilpikonna ee308d29ff [S] Optimize x axis label 2019-11-09 19:39:53 -05:00
Hykilpikonna 35ef2144b7 [O] Limit minimum zoom 2019-11-09 19:29:45 -05:00
Hykilpikonna 6dc2a21e8f [+] Add back tooltip 2019-11-09 17:42:03 -05:00
Hykilpikonna 343c921eb2 [+] Add y axis label 2019-11-09 17:40:15 -05:00
Hykilpikonna f54dfba058 [F] Fix scatter plot out of bounds issue 2019-11-09 17:39:58 -05:00
Hykilpikonna a7d7ef44a6 [-] Remove debug output 2019-11-09 17:35:29 -05:00
Hykilpikonna 793d7444b2 [O] Add back the term lines and mark areas 2019-11-09 17:35:22 -05:00
Hykilpikonna eaf4d2ce7b [O] Make y max an integer 2019-11-09 17:32:17 -05:00
Hykilpikonna 79e615ee46 [O] Use graded courses only 2019-11-09 17:31:32 -05:00
Hykilpikonna 40353cfd35 [O] Generate settings on created 2019-11-09 17:31:22 -05:00
Hykilpikonna 2be2ce98e6 [F] Fix date conversion 2019-11-09 17:27:45 -05:00
Hykilpikonna 6659f65763 [F] Fix multiple assignment creating multiple dates issue 2019-11-09 17:19:30 -05:00
Hykilpikonna 5b3ba4db07 [+] Create method to convert data points to date points 2019-11-09 17:11:35 -05:00
Hykilpikonna a05281f4e4 [+] Separate method to generate series 2019-11-09 17:10:54 -05:00
Hykilpikonna 2f95548fb3 [+] Create method to get series for course 2019-11-09 17:10:37 -05:00
Hykilpikonna 96ee9e9265 [O] Default to 2 decimal places 2019-11-09 17:05:06 -05:00
Hykilpikonna 9026b9d3a9 [-] Remove deprecated convertChart() 2019-11-09 17:02:53 -05:00
Hykilpikonna 67e38dd554 [F] Fix tooltip date representation 2019-11-09 16:59:52 -05:00
Hykilpikonna 7ffef67e42 [F] Parse time instead of date 2019-11-09 16:57:12 -05:00
Hykilpikonna 0d195cfb7f [F] Fix invalid date 2019-11-09 16:56:22 -05:00
Hykilpikonna 0d3e9c0840 [O] Remove toChartDate, use timestamp instead 2019-11-09 16:53:53 -05:00
Hykilpikonna 6c600f31f8 [O] Change assignment.date to time 2019-11-09 16:50:48 -05:00
Hykilpikonna a05a44aaef [+] Add assignments.length > 0 as a condition 2019-11-09 16:49:30 -05:00
Hykilpikonna 31d3a5a09e [+] Filter graded courses 2019-11-09 16:48:45 -05:00
Hykilpikonna 30293cd261 [O] Encapsulate method to get graded assignments 2019-11-09 16:31:36 -05:00
Hykilpikonna 9a279b3417 [O] Use afterconfig 2019-11-09 16:24:00 -05:00
Hykilpikonna 4a7ff0ea55 [O] Use base settings in overall-line 2019-11-09 16:23:41 -05:00
Hykilpikonna fd341e9d33 [O] Use basesettings in course-scatter 2019-11-09 16:15:21 -05:00
Hykilpikonna bdacc8dd9e [+] Create function to generate base settings 2019-11-09 16:14:58 -05:00
Hykilpikonna ad2c8a1ee8 [M] Move dot to graph utils 2019-11-09 16:12:44 -05:00
Hykilpikonna 1f58818a1a [S] Dim markline brightness 2019-11-09 16:06:28 -05:00
Hykilpikonna 09442cbfba [+] Add mark areas and lines in scatter plot 2019-11-09 16:05:55 -05:00
Hykilpikonna 91e10d1fa8 [+] Add opacity param to markline generation 2019-11-09 16:05:25 -05:00
Hykilpikonna 80267feb54 [O] Separate series creation with the main settings 2019-11-09 16:00:57 -05:00
Hykilpikonna f4ee2dadb6 [M] Move markarea to GraphUtils 2019-11-09 15:51:05 -05:00
Hykilpikonna d698f3d13a [M] Move markline stuff to GraphUtils 2019-11-09 15:47:30 -05:00
Hykilpikonna 82cb845061 [S] Make line animation faster 2019-11-09 15:43:53 -05:00
Hykilpikonna 940738307b [F] Fix term 1 graph displaying dates after term 2 2019-11-09 15:41:54 -05:00
Hykilpikonna df011787e1 [S] Change line color 2019-11-09 15:41:12 -05:00
Hykilpikonna 0af393a1e4 [+] Put in actual term dates 2019-11-09 15:37:52 -05:00
Hykilpikonna 6504cc033b [+] Encapsulate method to get term end date 2019-11-09 15:32:25 -05:00
Hykilpikonna c6ccc5e311 [F] Fix: All year graph begins at term 2 2019-11-09 15:28:48 -05:00
Hykilpikonna 3a8899507f [+] Encapsulate method to get begin date of a term 2019-11-09 15:28:21 -05:00
Hykilpikonna 2b7026c4ce [F] Fix: Make starting time not exceed the beginning of a term 2019-11-09 15:24:41 -05:00
Hykilpikonna a3fd822252 [S] Remove quater line arrow symbol 2019-11-09 15:05:57 -05:00
Hykilpikonna f9d7fa398f [+] Add quater line label 2019-11-09 14:59:38 -05:00
Hykilpikonna 7ffc445bba [+] Show quater lines 2019-11-09 14:55:47 -05:00
Hykilpikonna 8e9f6a4bb7 [O] Optimize date data type 2019-11-09 14:55:37 -05:00
Hykilpikonna 05cb560c8c [-] Disable legend 2019-11-09 14:54:35 -05:00
Hykilpikonna 53a57234a0 [-] Remove unused legend 2019-11-09 14:54:02 -05:00
Hykilpikonna 6af6bb0959 [O] Optimize time display 2019-11-09 14:47:32 -05:00
Hykilpikonna 089aad7398 [O] Optimize date formatting 2019-11-09 14:47:22 -05:00
Hykilpikonna 937e89ce5f [M] Move assignment interface from app.ts to course.ts 2019-11-09 14:38:57 -05:00
Hykilpikonna b0685ffd6b [M] Move util classes to /logic 2019-11-09 14:32:45 -05:00
Hykilpikonna 18dee50b96 [F] Force desktop view on mobile devices 2019-11-09 14:32:10 -05:00
Hykilpikonna 8dbef09ec9 [U] Release v0.4.2.912 2019-11-07 22:29:01 -05:00
Hykilpikonna 73b71f56a5 [S] Disable text selecting 2019-11-07 22:26:43 -05:00
Hykilpikonna ce702405d0 [S] Adjust no-grade font size 2019-11-07 22:26:33 -05:00
Hykilpikonna 21744a1bef [F] Fix gpa -1 when no grade issue 2019-11-07 22:26:13 -05:00
Hykilpikonna 82064f2f02 [O] Optimize graph max value 2019-11-07 22:14:46 -05:00
Hykilpikonna adfebc8c44 [F] Fix term 1 displaying term 2 grades issue 2019-11-07 22:00:08 -05:00
Hykilpikonna 2f30e67671 [F] Fix course grade displayed as 0.00 issue 2019-11-07 21:53:16 -05:00
Hykilpikonna 0a80d534eb [O] Optimize imports 2019-11-07 21:48:40 -05:00
Hykilpikonna a77c495843 [S] Further divide marklines 2019-11-07 21:45:34 -05:00
Hykilpikonna 5503aff6b1 [S] Dim marklines a little bit 2019-11-07 21:40:16 -05:00
Hykilpikonna 7cc4567245 [O] Optimize color representation 2019-11-07 21:39:09 -05:00
Hykilpikonna e7f29ad0bf [+] Add mark area below 70 2019-11-07 21:35:45 -05:00
Hykilpikonna cdbd101428 [+] Add mark area from 70 to 80 2019-11-07 21:33:22 -05:00
Hykilpikonna da650ef16b [+] Add mark area from 100 to 80 2019-11-07 21:31:51 -05:00
Hykilpikonna 8abfdd7f8e [+] Add mark area above 100 2019-11-07 21:31:36 -05:00
Hykilpikonna 3c66f99363 [F] Limit graph max value to 120 2019-11-07 20:57:35 -05:00
Hykilpikonna 251f87a072 [F] Fix assignment type update problem 2019-11-06 23:09:32 -05:00
Hykilpikonna e15c95561c [U] Release v0.4.1.895 2019-11-06 22:34:08 -05:00
Hykilpikonna f4ed39401c [S] Emphesize the title more 2019-11-06 22:29:03 -05:00
Hykilpikonna c3fb998254 [S] Set font size 2019-11-06 22:25:03 -05:00
Hykilpikonna d9e9bcc731 [S] Align the average span to the left 2019-11-06 22:24:56 -05:00
Hykilpikonna 110ff6daec [S] Center the average span 2019-11-06 22:24:46 -05:00
Hykilpikonna 7d83344e73 [F] Calculate percent average instead of ratio 2019-11-06 22:19:29 -05:00
Hykilpikonna 06d93398fb [S] Disable unread in courses page 2019-11-06 22:18:44 -05:00
Hykilpikonna b05328888e [M] Rename unread-entry class to assignment-entry 2019-11-06 22:17:25 -05:00
Hykilpikonna 19598f4a10 [+] Add average span 2019-11-06 22:16:59 -05:00
Hykilpikonna cd7102a5df [+] create method to get type average 2019-11-06 22:16:52 -05:00
Hykilpikonna 9bf1f6b624 [O] Optimize assignments filtering 2019-11-06 21:54:41 -05:00
Hykilpikonna f1681ad405 [F] Fix 'selectedTime undefined' 2019-11-06 21:54:11 -05:00
Hykilpikonna e0a70a23b0 [F] Fix: ' Invalid handler for event "command": got undefined' 2019-11-06 21:53:40 -05:00
Hykilpikonna 677dfb491a [+] Parse grading period 2019-11-06 21:50:28 -05:00
Hykilpikonna 07a9b38c8d [+] Add grading period field to app.ts 2019-11-06 21:50:22 -05:00
Hykilpikonna 53b78c4a94 [M] Rename cookie name too 2019-11-06 21:50:06 -05:00
Hykilpikonna 2057f08e40 [M] Rename time selection to grading period 2019-11-06 21:49:17 -05:00
Hykilpikonna 7bc843aa9a [O] Optimize assignments loading with [].map() 2019-11-06 21:41:49 -05:00
Hykilpikonna 0a8855e020 [F] Fix self-referencing error 2019-11-06 21:37:13 -05:00
Hykilpikonna 445d07c5e4 [+] Encapsulate method to get numeric and letter grades 2019-11-06 21:35:34 -05:00
Hykilpikonna 125e7d14de [O] Change letterGrade and numericGrade to raw fields 2019-11-06 21:32:55 -05:00
Hykilpikonna 4917f5cb0c [O] Optimize getGP 2019-11-06 21:32:38 -05:00
Hykilpikonna 2504b37eb7 [O] Also support numeric grade 2019-11-06 21:30:15 -05:00
Hykilpikonna b0d3bf4bd2 [+] Encapsulate method to find scale 2019-11-06 21:28:27 -05:00
Hykilpikonna e640384fd8 [O] Optimize scale list 2019-11-06 21:27:30 -05:00
Hykilpikonna adf7af6b84 [+] Create interface for scale 2019-11-06 21:27:19 -05:00
Hykilpikonna beb4155aab [+] Compare numeric grade with current term grade 2019-11-06 21:17:31 -05:00
Hykilpikonna 3337766ae6 [+] Add current term constant 2019-11-06 21:17:11 -05:00
Hykilpikonna dc135dc78b [+] Encapsulate getPercentTypeAverage 2019-11-06 21:16:47 -05:00
Hykilpikonna b39c51beea [O] getTotalMeanAverage pass in assignments not course 2019-11-06 21:16:31 -05:00
Hykilpikonna 971339e49c [F] Fix assignments order issue 2019-11-06 20:59:12 -05:00
Hykilpikonna 9db0431e96 [O] Use caching to optimize graphs 2019-11-06 20:57:52 -05:00
Hykilpikonna d667f2ab34 [-] Remove temp string 2019-11-06 20:39:58 -05:00
Hykilpikonna 9b895a8fd7 [+] Reload directly when selected time is changed 2019-11-06 20:39:47 -05:00
Hykilpikonna 66d7e18a71 [F] Fix selectedTime initialization issue 2019-11-06 20:30:05 -05:00
Hykilpikonna 4643155908 [+] Add useless string for updating 2019-11-06 20:29:39 -05:00
Hykilpikonna c6c3303a99 [O] Optimize onclick method call 2019-11-06 20:02:41 -05:00
Hykilpikonna 1aea558c40 [+] Store selected time in cookies 2019-11-06 20:02:29 -05:00
Hykilpikonna 316c7e1f63 [F] Fix 'cannot getTime() of undefined' 2019-11-06 20:01:32 -05:00
Hykilpikonna 8364befc91 [F] Fix more type problems 2019-11-06 19:26:41 -05:00
Hykilpikonna 47c25d0e71 [F] Fix imports 2019-11-06 19:23:39 -05:00
Hykilpikonna 422d574c6c [F] Fix term display number offset issue 2019-11-06 19:14:14 -05:00
Hykilpikonna 3400c07e77 [+] Encapsulate method to get assignments of specific time 2019-11-06 19:13:45 -05:00
Hykilpikonna 79bb10b14e [M] Rename assignments to rawAssignments 2019-11-06 19:12:24 -05:00
Hykilpikonna e7563fcfb5 [+] Encapsulate method to get selected time code 2019-11-06 19:11:55 -05:00
Hykilpikonna 43f0046827 [+] Update selected time dynamically 2019-11-06 19:11:41 -05:00
Hykilpikonna ef424dd9e3 [O] Unify time names 2019-11-06 19:11:27 -05:00
Hykilpikonna 37902b6d1f [+] Display selected time dynamically 2019-11-06 19:11:13 -05:00
Hykilpikonna 92e692f69a [O] Remove getGradedCourses 2019-11-06 18:27:19 -05:00
Hykilpikonna b6e0e12cab [+] Add isGraded method to course 2019-11-06 18:25:48 -05:00
Hykilpikonna 6eb7e421e0 [M] Move level detection to course 2019-11-06 18:22:35 -05:00
Hykilpikonna fbb9352546 [O] Move name parsing to course 2019-11-06 18:15:54 -05:00
Hykilpikonna 6f5c4f3a09 [M] Move loadAssignments to course 2019-11-06 18:13:24 -05:00
Hykilpikonna 1e91cec8d2 [O] Create course objects after parsing json 2019-11-06 18:08:47 -05:00
Hykilpikonna 17262e12af [+] Create constructor 2019-11-06 18:05:53 -05:00
Hykilpikonna 56a954c235 [O] Separate course object 2019-11-06 18:03:27 -05:00
Hykilpikonna da5eced769 [F] Fix pushed data duplicate issue 2019-11-06 17:55:10 -05:00
Hykilpikonna 71f2eccab4 [F] Fix filter assignment order issue 2019-11-06 17:54:57 -05:00
Hykilpikonna a5162c1f5b [O] Optimize date usage 2019-11-06 17:38:53 -05:00
Hykilpikonna 53c82fd477 [F] Fix sorting 2019-11-06 17:38:36 -05:00
Hykilpikonna 2867f8d09c [+] Set computed data 2019-11-06 17:26:52 -05:00
Hykilpikonna 5b902171c8 [+] Filter assignments into terms 2019-11-06 17:26:44 -05:00
Hykilpikonna c40c5b6b94 [+] Create computed model in course 2019-11-06 17:26:29 -05:00
Hykilpikonna b66c313b05 [+] Define terms date list 2019-11-06 17:20:09 -05:00
Hykilpikonna 48287cdc4b [O] Sort by date 2019-11-06 17:17:02 -05:00
Hykilpikonna 750c768848 [O] Use Date for date instead of string 2019-11-06 17:16:49 -05:00
Hykilpikonna 4b512d64d9 [+] Add commands to dropdown options 2019-11-06 16:48:43 -05:00
Hykilpikonna e121210e96 [S] Add proper up and down margins 2019-11-06 16:34:08 -05:00
Hykilpikonna 5a87608fa6 [S] Align dropdown to the right 2019-11-06 16:33:59 -05:00
Hykilpikonna e819abe789 [S] Make dropdown button smaller 2019-11-06 16:33:42 -05:00
Hykilpikonna eb718d14d1 [S] Add id to dropdown 2019-11-06 16:33:35 -05:00
Hykilpikonna 564896e940 [+] Create time selection button 2019-11-06 16:29:02 -05:00
Hykilpikonna d91c3875b0 [F] Fix bar graph color problem 2019-11-06 16:18:39 -05:00
Hykilpikonna 1fe2edb9f4 [F] Fix bar graph -1 problem 2019-11-06 16:17:31 -05:00
Hykilpikonna 91977e1226 [S] Change assignment type head font size 2019-11-06 16:06:33 -05:00
Hykilpikonna 948018c7de [F] Fix grade display when no grade exist 2019-11-06 16:06:03 -05:00
Hykilpikonna f2350680c8 [S] Optimize narrow height 2019-11-05 22:57:19 -05:00
Hykilpikonna 4cc424e079 [S] Make percentage score's width unified 2019-11-05 22:57:09 -05:00
Hykilpikonna 49d26fcf61 [S] Make month's width unified 2019-11-05 22:56:51 -05:00
Hykilpikonna 9fde3b21d4 [S] Fix margins when there are no unread 2019-11-05 22:51:47 -05:00
Hykilpikonna 6952649058 [S] Optimize date formatting 2019-11-05 22:51:15 -05:00
Hykilpikonna b12a717cea [S] Move unread property to css class 2019-11-05 22:41:33 -05:00
Hykilpikonna 8e1e222656 [S] Align the scores the same width 2019-11-05 22:39:57 -05:00
Hykilpikonna 8f7775bb69 [O] Reduce height for narrow layout 2019-11-05 22:33:22 -05:00
Hykilpikonna 0477161af2 [+] Create narrow property for class 2019-11-05 22:29:43 -05:00
Hykilpikonna 3b387db5b5 [S] Fix float left alignment issue 2019-11-05 22:24:45 -05:00
Hykilpikonna d85d0e4e36 [S] Align the text to the left 2019-11-05 22:23:25 -05:00
Hykilpikonna b74b086f98 [S] Vertically center the text 2019-11-05 22:23:14 -05:00
Hykilpikonna e1d20b822f [S] Optimize font size and color 2019-11-05 22:22:53 -05:00
Hykilpikonna e98997f230 [S] Specify background color 2019-11-05 18:25:56 -05:00
Hykilpikonna 82e17030d4 [+] Add property for background style 2019-11-05 18:22:44 -05:00
Hykilpikonna d1905ee2b1 [+] Create info div 2019-11-05 18:09:12 -05:00
Hykilpikonna 310b2bcee1 [S] Remove card body padding 2019-11-05 18:07:49 -05:00
Hykilpikonna d55eebce19 [F] Fix: Next course doesn't show up on mobile devices 2019-11-05 18:00:04 -05:00
Hykilpikonna 03507968ee [+] Add assignment entry components 2019-11-05 17:51:36 -05:00
Hykilpikonna 56769e4518 [F] Fix: Filter assignments based on value of type 2019-11-05 17:51:22 -05:00
Hykilpikonna fba589d6fe [+] Display type name 2019-11-05 17:44:16 -05:00
Hykilpikonna ff5b22b5dc [+] Filter assignment type 2019-11-05 17:42:46 -05:00
Hykilpikonna 14de2e0b23 [+] Create assignments property 2019-11-05 17:40:50 -05:00
Hykilpikonna 880e331c99 [F] Fix distinct key for v-for problem 2019-11-05 17:37:42 -05:00
Hykilpikonna cf794588df [-] Reomve duplicate type names 2019-11-05 17:37:29 -05:00
Hykilpikonna d362598d83 [+] Show an assignment type for each type 2019-11-05 17:29:27 -05:00
Hykilpikonna 536e98642d [+] Encapsulate method to get assignment types 2019-11-05 17:28:28 -05:00
Hykilpikonna baae0d088c [+] Add assignment head element in course page 2019-11-05 17:28:18 -05:00
Hykilpikonna 202feb12d7 [+] Create assignment type head vue comp 2019-11-05 17:22:13 -05:00
Hykilpikonna d2ab7c059f [F] Fix clear cookie issue 2019-11-04 16:23:40 -05:00
Hykilpikonna e1ef117dcd [S] Make logo non-clickable 2019-11-03 11:18:35 -05:00
Hykilpikonna fb3186d575 [S] Add shadow effect to logo 2019-11-03 11:18:26 -05:00
Hykilpikonna b913873951 [S] Adjust margin font size 2019-11-03 11:18:01 -05:00
Hykilpikonna bbdcb42316 [S] Adjust title margin size 2019-11-03 11:17:53 -05:00
Hykilpikonna 5438637224 [S] Add the title 2019-11-03 11:17:33 -05:00
Hykilpikonna 970a058ba3 [U] Pre-release v0.3.6.784 2019-11-03 10:44:57 -05:00
Hykilpikonna 5d295db1b7 [O] Add todo: update token each access 2019-11-03 10:43:50 -05:00
Hykilpikonna b22aac7ca2 [F] Fix cookie expiration issue 2019-11-03 10:26:10 -05:00
Hykilpikonna eef29a4611 [U] Add 'Unknown' to graded courses detection 2019-11-02 23:10:58 -04:00
Hykilpikonna 131952ed37 [O] Trim course names 2019-11-02 23:07:13 -04:00
Hykilpikonna 2e95c2550e [+] Create 404 redirection 2019-11-02 23:04:05 -04:00
Hykilpikonna 0db6e0d693 [S] Add some page end margins 2019-11-02 22:58:37 -04:00
Hykilpikonna 27198ad4e0 [U] Use CourseUtils.postProcess() 2019-11-02 22:57:41 -04:00
Hykilpikonna 3cbb6ebee6 [+] Add "HS" clubs to level detection 2019-11-02 22:56:27 -04:00
Hykilpikonna 2308df65b5 [+] Add post process for courses 2019-11-02 22:54:30 -04:00
Hykilpikonna c8f82cc991 [+] Use unknown course list 2019-11-02 22:54:02 -04:00
Hykilpikonna a6e1b905ed [O] Make unknown course list a map 2019-11-02 22:49:49 -04:00
Hykilpikonna 9432e9a806 [+] Create method to detect course level 2019-11-02 22:46:21 -04:00
Hykilpikonna d90280a10f [+] Create unknown course list 2019-11-02 22:46:04 -04:00
Hykilpikonna db30b7f807 [+] Create club constant 2019-11-02 22:45:52 -04:00
Hykilpikonna 20910b1562 [+] Add level constants 2019-11-02 22:18:58 -04:00
Hykilpikonna 5659a049e5 [O] Parse & in course names 2019-11-02 20:41:11 -04:00
Hykilpikonna 085812d859 [+] Create method to parse html text 2019-11-02 20:37:41 -04:00
Hykilpikonna e60a4669ac [F] Fix execution order problem 2019-11-02 20:21:26 -04:00
Hykilpikonna ac84907a98 [F] Update index only after loading 2019-11-02 20:20:09 -04:00
Hykilpikonna 04ee69e8e2 [F] Fix clickable required issue 2019-11-02 20:12:14 -04:00
Hykilpikonna a2445aca6a [F] Fix path reading issue 2019-11-02 20:11:39 -04:00
Hykilpikonna 336a58b23d [O] Move course head scss to separate file 2019-11-02 20:02:06 -04:00
Hykilpikonna 5a5cf9bd4d [O] Replace more ts-ignores with {required: true} 2019-11-02 20:00:19 -04:00
Hykilpikonna 44d262f457 [O] Inline course-page.ts 2019-11-02 19:58:25 -04:00
Hykilpikonna 27fda43373 [O] Inline overall.ts 2019-11-02 19:56:01 -04:00
Hykilpikonna b3de3b8405 [+] Only show mark as read if it is unread 2019-11-02 19:45:30 -04:00
Hykilpikonna 633918aa40 [U] Pass in unread value 2019-11-02 19:45:11 -04:00
Hykilpikonna 3bc59e87d3 [O] Inline assignment entry ts script 2019-11-02 19:41:55 -04:00
Hykilpikonna af7d9e9dca [+] Add unread prop to assignment entry 2019-11-02 19:40:26 -04:00
Hykilpikonna 3b8884dc84 [O] Optimize imports 2019-11-02 19:39:33 -04:00
Hykilpikonna 92158684c0 [-] Remove unnecessary ts-ignores 2019-11-02 19:38:45 -04:00
Hykilpikonna b37de9cf24 [O] Disable strict property initialization check 2019-11-02 19:37:13 -04:00
Hykilpikonna cd2dff5559 [M] Rename unread-entry to assignment-entry 2019-11-02 19:35:04 -04:00
Hykilpikonna d6a85af15c [O] Use relative date for starting value 2019-11-02 19:24:51 -04:00
Hykilpikonna 0d333879ca [O] Put login in a form 2019-11-02 19:08:02 -04:00
Hykilpikonna 905db3c73a [-] Remove debug logging 2019-11-02 19:04:56 -04:00
Hykilpikonna c5dfad8be8 [F] Remove trapezoid effect for mobile support 2019-11-02 19:03:35 -04:00
Hykilpikonna c9454a3832 [-] Remove todos that are done 2019-11-02 19:02:38 -04:00
Hykilpikonna c732d475f1 [O] Log when cookies doesn't exist 2019-10-23 20:20:54 -04:00
Hykilpikonna a4b7e0fd46 [O] Only clear cookies if cookies exists 2019-10-23 20:20:41 -04:00
Hykilpikonna bcee069b32 [F] Fix url index issue 2019-10-21 19:23:41 -04:00
Hykilpikonna 182208f8c3 [F] Fix Github Pages sub path issue by using # after url 2019-10-21 19:23:31 -04:00
70 changed files with 6168 additions and 3750 deletions
+3
View File
@@ -1,5 +1,8 @@
#!/usr/bin/env bash
#echo "Switch to production database settings"
#exit
# abort on errors
set -e
Binary file not shown.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

+3435 -2613
View File
File diff suppressed because it is too large Load Diff
+14 -10
View File
@@ -8,24 +8,28 @@
"lint": "vue-cli-service lint"
},
"dependencies": {
"core-js": "^2.6.5",
"echarts": "^4.2.1",
"element-ui": "^2.11.1",
"@types/chroma-js": "^1.4.3",
"@types/md5": "^2.1.33",
"chroma-js": "^2.1.0",
"core-js": "^2.6.10",
"echarts": "^4.5.0",
"element-ui": "^2.13.0",
"md5": "^2.2.1",
"moment": "^2.24.0",
"p-wait-for": "^3.1.0",
"v-charts": "^1.19.0",
"vue": "^2.6.10",
"vue-class-component": "^7.0.2",
"vue-cookies": "^1.5.13",
"vue-property-decorator": "^8.1.0"
"vue-property-decorator": "^8.3.0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^3.10.0",
"@vue/cli-plugin-typescript": "^3.10.0",
"@vue/cli-service": "^3.10.0",
"node-sass": "^4.9.0",
"sass-loader": "^7.1.0",
"typescript": "^3.4.3",
"@vue/cli-plugin-babel": "^3.12.1",
"@vue/cli-plugin-typescript": "^3.12.1",
"@vue/cli-service": "^4.1.1",
"node-sass": "^4.13.0",
"sass-loader": "^7.3.1",
"typescript": "^3.7.3",
"vue-template-compiler": "^2.6.10"
},
"postcss": {
+14
View File
@@ -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>
+6 -3
View File
@@ -3,9 +3,10 @@
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<!--meta name="viewport" content="width=device-width,initial-scale=1.0"-->
<meta name="viewport" content="width=1024">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<link rel="icon" href="<%= BASE_URL %>logo@32px.png">
<title>Veracross Analyzer</title>
</head>
@@ -23,7 +24,9 @@
<!-- ElementUI -->
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<!-- Fonts -->
<link href="https://fonts.googleapis.com/css?family=Nunito+Sans&display=swap" rel="stylesheet">
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
+14
View File
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Redirecting...</title>
<meta http-equiv = "refresh" content = "0; url = https://vera.hydev.org/#info" />
</head>
<body>
Redirecting to (<a href="https://vera.hydev.org/#info">https://vera.hydev.org/#info</a>)...
<script>
window.location.href = 'https://vera.hydev.org/#info';
</script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 44 KiB

+64 -1
View File
@@ -5,6 +5,7 @@
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
padding-bottom: 100px;
}
#app-content
@@ -24,7 +25,58 @@
--assignment-type-3: #ff9900;
--assignment-type-4: #b02b02;
--font: 'Avenir', Helvetica, Arial, sans-serif;
//--font: 'Avenir', Helvetica, Arial, sans-serif;
--font: -apple-system, Nunito Sans, Avenir, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue,
Hiragino Sans GB, Microsoft YaHei, WenQuanYi Micro Hei, Helvetica, Arial, sans-serif;
}
.dark
{
--dark-layer-1: #383838;
--dark-layer-2: #525252;
--dark-layer-3: #6c6c6c;
--dark-foreground: #e9e9e9;
background: var(--dark-layer-1) !important;
div, ul
{
background: var(--dark-layer-2) !important;
color: var(--dark-foreground) !important;
}
span, button
{
color: var(--dark-foreground) !important;
}
.el-card
{
border: none !important;
}
// Overall
#app-inner, #overall, #overall-course, .overall-span, #app-content
{
background: var(--dark-layer-1) !important;
}
// Course card
.entry-box, .none .unread-number {background: #797979 !important}
.entry-box.max {background-color: #949494 !important}
.entry-box.percent {background-color: #a7a490 !important}
.course-name {color: #cffff6 !important}
#block-grade #updates.none #unread-number {background: #757575 !important}
.course-card-content.expand, .assignment-entry, .unread-row,
.unread-row .el-col, #assignment-type-head, .course-page-graph.el-col
{
background-color: var(--dark-layer-3) !important;
}
// Nav bar
.el-menu--horizontal>.el-menu-item.is-active {color: var(--dark-foreground) !important;}
}
// ##############
@@ -82,3 +134,14 @@ div.el-card.course-card > div.el-card__body
-ms-user-select: none;
user-select: none;
}
.el-dropdown-menu__item
{
font-family: Nunito Sans, Helvetica Neue, Microsoft YaHei, "微软雅黑", Arial, sans-serif;
}
// Fix word breaking
.el-dialog__body
{
word-break: unset !important;
}
+89 -104
View File
@@ -1,117 +1,83 @@
import {Component, Vue} from 'vue-property-decorator';
import Login from '@/components/login/login';
import Navigation from '@/components/navigation/navigation';
import Overall from '@/pages/overall/overall';
import Overall from '@/pages/overall/overall.vue';
import Constants from '@/constants';
import JsonUtils from '@/utils/json-utils';
import pWaitFor from 'p-wait-for';
import {HttpUtils} from '@/utils/http-utils';
import {CourseUtils} from '@/utils/course-utils';
import {GPAUtils} from '@/utils/gpa-utils';
import Loading from '@/components/loading/loading.vue';
import CoursePage from '@/pages/course/course-page';
/**
* Objects of this interface represent assignment grades.
*/
export interface Assignment
{
id: number,
scoreId: number,
type: string,
typeId: number,
description: string,
date: string,
complete: string,
include: boolean,
display: boolean,
unread: boolean,
scoreMax: number,
score: number
}
/**
* A course
*/
export interface Course
{
assignmentsId: number,
id: number,
name: string,
teacherName: string,
status: string,
letterGrade?: string,
numericGrade?: number,
level: string,
scaleUp: number,
grading:
{
method: string,
weightingMap: {[index: string]: number}
}
assignments: Assignment[]
}
import {HttpUtils} from '@/logic/utils/http-utils';
import Loading from '@/components/overlays/loading.vue';
import CoursePage from '@/pages/course/course-page.vue';
import Course from '@/logic/course';
import LoginUser from '@/logic/login-user';
import NavController from '@/logic/nav-controller';
import Info from '@/statics/Info.vue';
@Component({
components: {Login, Navigation, Overall, Loading, CoursePage},
components: {Login, Navigation, Overall, Loading, CoursePage, Info},
})
export default class App extends Vue
{
// Is the login panel shown
public showLogin: boolean = true;
showLogin: boolean = true;
// List of course that the student takes
public courses: Course[] = [];
// List of course that should be displayed
public filteredCourses: Course[] = [];
// Currently selected tab
public selectedTab: string = 'overall';
courses: Course[] = [];
gradedCourses: Course[] = [];
// Are the course assignments loaded from the server.
public assignmentsReady: boolean = false;
assignmentsReady: boolean = false;
// Token
public token: string = '';
user: LoginUser = null as any;
// Loading text
public loading: string = '';
loading: string = '';
// Loading error
public loadingError: boolean = false;
loadingError: boolean = false;
// Navigation controller
nav: NavController = new NavController();
// Http Client
public static http: HttpUtils = new HttpUtils();
static http: HttpUtils = new HttpUtils();
// Instance
public static instance: App;
static instance: App;
// Static page
staticPage: string = '';
// Dark mode
darkMode: boolean = false;
/**
* This is called when the instance is created.
*/
public created()
created()
{
// Show splash
console.log(Constants.SPLASH);
// Update instance
App.instance = this;
// Check location
if (window.location.hash == '#info')
{
this.staticPage = 'info';
}
// Dark mode
this.darkMode = this.$cookies.isKey('dark');
}
/**
* This is called when the user logs in.
*
* @param token Authorization token
* @param user Authorization user
*/
public onLogin(token: string)
onLogin(user: LoginUser)
{
// Hide login bar
this.showLogin = false;
@@ -119,20 +85,20 @@ export default class App extends Vue
// Show loading message
this.logLoading('1. Logging in...');
// Store token
this.token = token;
// Store user
this.user = user;
// Assign token to http client
App.http.token = token;
// Assign user to http client
App.http.user = user;
// Load data
this.loadCoursesAfterLogin();
}
/**
* Load courses data after login.
* Load courses data after logging in.
*/
public loadCoursesAfterLogin()
loadCoursesAfterLogin()
{
// Show loading message
this.logLoading('2. Loading courses...');
@@ -144,7 +110,7 @@ export default class App extends Vue
if (response.success)
{
// Save courses
this.courses = response.data;
this.courses = response.data.map((courseJson: any) => new Course(courseJson));
// Load assignments
this.loadAssignments();
@@ -157,7 +123,7 @@ export default class App extends Vue
/**
* Load the assignments of the courses
*/
public loadAssignments()
loadAssignments()
{
// Show loading message
this.logLoading('3. Loading assignments...');
@@ -171,9 +137,7 @@ export default class App extends Vue
// Check success
if (response.success)
{
// Load assignments
// Parse json and filter it
course.assignments = JsonUtils.filterAssignments(response.data);
course.loadAssignments(response.data);
}
else throw new Error(response.data);
})
@@ -181,10 +145,10 @@ export default class App extends Vue
});
// Wait for assignments to be ready.
pWaitFor(() => this.courses.every(c => c.assignments != null)).then(() =>
pWaitFor(() => this.courses.every(c => c.rawAssignments != null)).then(() =>
{
// Filter courses
this.filteredCourses = CourseUtils.getGradedCourses(this.courses);
this.gradedCourses = this.courses.filter(c => c.isGraded);
// Check grading algorithms
this.checkGradingAlgorithms();
@@ -194,40 +158,50 @@ export default class App extends Vue
/**
* Check the courses' grading algorithms. (Total-mean or percent-type)
*/
private checkGradingAlgorithms()
checkGradingAlgorithms()
{
// Show loading message
this.logLoading('4. Checking grading algorithms...');
// Loop through all the courses
for (const course of this.filteredCourses)
for (const course of this.gradedCourses)
{
// Check if total-average grade is the same with percent-type grade
if (course.numericGrade == +GPAUtils.getTotalMeanAverage(course).toFixed(2))
for (const i of [0, 1, 2, 3])
{
course.grading = {method: 'TOTAL_MEAN', weightingMap: {}};
}
else
{
// Request grading scheme for this course
App.http.post('/grading', {'assignmentsId': course.assignmentsId}).then(response =>
const cookieIndex = `va.grading.${i}.${course.assignmentsId}`;
// Check if already exist in cookies
if (this.$cookies.isKey(cookieIndex))
{
course.termGrading[i] = {method: 'TOTAL_MEAN', weightingMap: {}};
continue;
}
// Request grading scheme for this course at this grading period
App.http.post('/grading/term', {assignmentsId: course.assignmentsId, term: i}).then(resp =>
{
// Check success
if (response.success)
if (resp.success)
{
// Add it to course
course.grading = response.data;
course.termGrading[i] = resp.data;
// If it's total_mean, cache it to cookies
// This is because only percent_type can update over time
if (course.termGrading[i].method == 'TOTAL_MEAN')
{
this.$cookies.set(cookieIndex, 'TOTAL_MEAN', 'd');
}
}
else throw new Error(response.data);
else throw new Error(resp.data);
})
.catch(e => this.showError(`Error: Grading data failed to load.\n(${e})`))
}
}
// Wait for done
pWaitFor(() => this.filteredCourses.every(c => c.grading != undefined)).then(() =>
pWaitFor(() => this.gradedCourses.every(c => c.termGrading.every(g => g != null))).then(() =>
{
// When the assignments are ready
this.assignmentsReady = true;
// Remove loading
@@ -240,7 +214,7 @@ export default class App extends Vue
*
* @param message Message
*/
private logLoading(message: string)
logLoading(message: string)
{
if (message == '') this.loading = '';
else this.loading += '\n' + message;
@@ -251,7 +225,7 @@ export default class App extends Vue
*
* @param message Error message
*/
private showError(message: string)
showError(message: string)
{
this.loadingError = true;
this.loading = message;
@@ -260,7 +234,7 @@ export default class App extends Vue
/**
* Sign out
*/
public signOut()
signOut()
{
// Clear all cookies
this.$cookies.keys().forEach(key => this.$cookies.remove(key));
@@ -268,4 +242,15 @@ export default class App extends Vue
// Refresh
window.location.reload();
}
/**
* Select time (Eg. Term 1, Term 2, All Year, etc.)
*
* @param code
*/
selectTime(code: number)
{
// TODO: Optimize
window.location.reload();
}
}
+20 -14
View File
@@ -1,23 +1,29 @@
<template>
<div id="app" class="theme-default">
<login v-if="showLogin" v-on:login:token="onLogin"></login>
<navigation :courses="filteredCourses"
:activeIndex.sync="selectedTab"
v-on:sign-out="signOut">
</navigation>
<div id="app-inner" v-if="staticPage === ''" :class="{dark: darkMode}">
<login v-if="showLogin" v-on:login:user="onLogin"/>
<navigation v-if="user != null"
:courses="gradedCourses"
:user="user"
:nav="nav"
@sign-out="signOut" @select-time="selectTime">
</navigation>
<div id="app-content" v-if="assignmentsReady && loading === ''">
<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 id="app-content" v-if="assignmentsReady && loading === ''">
<overall v-if="nav.id === 'overall'"
:courses="gradedCourses">
</overall>
<course-page v-if="nav.id === 'course'"
:course="gradedCourses.find(c => +c.id === +nav.info.id)">
</course-page>
</div>
<loading v-if="loading !== ''" :text="loading" :error="loadingError"/>
</div>
<loading v-if="loading !== ''" :text="loading" :error="loadingError"></loading>
<Info v-if="staticPage === 'info'"/>
</div>
</template>
<script src="./app.ts" lang="ts"></script>
<style src="./app.scss" lang="scss"></style>
<style src="./app.scss" lang="scss"/>
+7
View File
@@ -5,6 +5,13 @@
}
// Logo image
#login-logo-image
{
width: 80%;
margin-bottom: -15px;
}
// Parent overlay
.login-overlay
{
+76 -42
View File
@@ -1,43 +1,44 @@
import {Component, Prop, Vue} from 'vue-property-decorator';
import {Component, Vue} from 'vue-property-decorator';
import Constants from '@/constants';
import {HttpUtils} from '@/utils/http-utils';
import App from '@/components/app/app';
import VersionUtils from '@/utils/version-utils';
import VersionUtils from '@/logic/utils/version-utils';
import LoginUser from '@/logic/login-user';
import Maintenance from '@/components/overlays/maintenance.vue';
/**
* This component handles user login, and obtains data from the server.
* TODO: Press enter to login
*/
@Component({
components: {},
})
@Component({components: {Maintenance}})
export default class Login extends Vue
{
public username: any = '';
public password: any = '';
username = '';
password = '';
public loading: boolean = false;
public error: String = '';
loading = false;
error = '';
disableInput = false;
maintenance = '';
/**
* This is called when the instance is created.
*/
public created()
created()
{
// Check cookies version
if (this.needToUpdateCookies())
{
console.log('Version Updated! Clearing cookies...');
// Clear all cookies
this.$cookies.keys().forEach(key => this.$cookies.remove(key));
}
// Check login cookies
if (this.$cookies.isKey('va.token'))
{
// Already contains valid token / TODO: Validate
this.$emit('login:token', this.$cookies.get('va.token'));
// Check cookies version
if (this.needToUpdateCookies()) this.clearCookies();
else
{
// Show loading
this.disableInput = this.loading = true;
// Login with token
this.login('/login/token', {token: this.$cookies.get('va.token')});
}
}
}
@@ -46,7 +47,7 @@ export default class Login extends Vue
*
* @returns boolean Need to clear cookies or not
*/
public needToUpdateCookies(): boolean
needToUpdateCookies(): boolean
{
// Version doesn't exist
if (!this.$cookies.isKey('va.version')) return true;
@@ -56,50 +57,83 @@ export default class Login extends Vue
}
/**
* On click, sends username and password to the server.
* When the user clicks, send the username and password to the server.
*/
public onLoginClick()
onLoginClick()
{
// Make login button loading
this.loading = true;
// Format it
this.username = this.username.toLowerCase().replace(/ /g, '').replace(/@.*/g, '');
// Login
this.login('/login', {username: this.username, password: this.password});
}
/**
* Actually post the login request and process the response
*
* @param url Posting url
* @param data Data to be posted
*/
login(url: string, data: any)
{
// Fetch request
App.http.post('/login', {username: this.username, password: this.password})
App.http.post(url, data)
.then(response =>
{
// Check success
if (response.success)
{
// Save token to cookies
this.$cookies.set('va.token', response.data, '27d');
this.$cookies.set('va.version', Constants.VERSION);
// Maintenance
if (response.data.maintenance)
{
this.maintenance = response.data.maintenance;
return;
}
// Call custom event with token
this.$emit('login:token', response.data);
// Save token to cookies
this.$cookies.set('va.token', response.data.user.token, '27d');
this.$cookies.set('va.version', Constants.VERSION, '27d');
// Call a custom event with the token
this.$emit('login:user', new LoginUser(response.data.user));
}
else
{
// Show error message
this.error = response.data;
// Login expired -> clear cookies
if (response.data == 'Error: Login expired')
{
this.clearCookies();
}
// Allow the user to retry
this.loading = false;
// Show error message & allow user to retry
this.error = response.data;
this.disableInput = this.loading = false;
}
})
.catch(err =>
{
alert(err);
// Allow the user to retry
this.loading = false;
// Show error message & allow user to retry
this.error = err;
this.disableInput = this.loading = false;
});
}
/**
* This is called when the user hits enter in the input boxes.
* This is called when the user hits enter on the input boxes.
*/
public onEnter()
onEnter()
{
this.onLoginClick();
}
/**
* Clear cookies
*/
clearCookies()
{
this.$cookies.keys().forEach(key => this.$cookies.remove(key));
}
}
+21 -16
View File
@@ -2,30 +2,35 @@
<div id="login" class="login-overlay">
<div class="login-vertical-center">
<div class="login-panel">
<img alt="Vue logo" src="../../assets/logo.png">
<img id="login-logo-image" alt="logo" src="../../assets/logo.png">
<h1>Veracross Analyzer</h1>
<form id="login-form">
<el-input v-model="username"
placeholder="SJP Username (Eg. flast21)"
:class="{'input-error': error !== ''}"
v-if="!disableInput"
@keyup.enter.native="onEnter">
</el-input>
<el-input v-model="username"
placeholder="School Username"
:class="{'input-error': error !== ''}"
@keyup.enter.native="onEnter">
</el-input>
<el-input v-model="password"
placeholder="SJP Password"
show-password=""
:class="{'input-error': error !== ''}"
v-if="!disableInput"
@keyup.enter.native="onEnter">
</el-input>
<el-input v-model="password"
placeholder="Veracross Password"
show-password=""
:class="{'input-error': error !== ''}"
@keyup.enter.native="onEnter">
</el-input>
<div class="el-form-item__error custom">{{error}}</div>
<div class="el-form-item__error custom">{{error}}</div>
<el-button plain type="primary" @click="onLoginClick" :loading="loading">Login</el-button>
<el-button plain type="primary" @click="onLoginClick" :loading="loading">Login</el-button>
</form>
</div>
</div>
<Maintenance v-if="maintenance" :message="maintenance"/>
</div>
</template>
<script src="./login.ts" lang="ts"></script>
<style src="./login.scss" lang="scss"></style>
<style src="./login.scss" lang="scss"/>
+68 -10
View File
@@ -16,6 +16,13 @@
}
}
#nav-avatar
{
position: absolute;
right: 0;
margin: 10px 20px;
}
#sign-out-button
{
// Float right
@@ -27,6 +34,17 @@
width: 110px;
}
#nav-grading-period
{
// Float right
position: absolute;
right: 80px;
// Margins
margin-top: 12px;
margin-bottom: 12px;
}
#nav-title
{
// Float left
@@ -37,15 +55,49 @@
height: 60px;
// Center text
display: inline-flex;
align-items: center;
// Margins
margin-left: 14px;
margin-left: 20px;
margin-right: 8px;
// Font
font-size: 1.25rem;
display: inline-flex;
// Make it non-clickable
pointer-events: none;
#nav-logo
{
height: 70%;
margin-right: 10px;
}
#nav-logo-text
{
// Color
color: #6bbeff !important;
background: linear-gradient(90deg,
rgba(90,177,239,1) 0%,
rgba(25,212,174,1) 100%) !important;
// Font
font-weight: 500;
font-size: 18px;
}
#nav-logo-text.logo-text
{
// Override the background
-webkit-text-fill-color: transparent !important;
-webkit-background-clip: text !important;
}
#nav-logo-version
{
color: #a7a7a7;
margin-left: 5px;
margin-top: 8px;
font-size: 10px;
}
}
#next-course
@@ -56,10 +108,17 @@
bottom: 0;
left: 25%;
padding-top: 2px;
box-shadow: 0 -2px 9px 0 #ecebeb;
box-shadow: 0 -2px 9px 0 #00000029;
}
// Trapezoid
transform: perspective(50px) rotateX(10deg);
footer
{
position: fixed;
left: 0;
bottom: 0;
width: 100%;
z-index: 1000;
}
#prev-course
@@ -70,10 +129,9 @@
top: 61px;
left: 25%;
padding-bottom: 2px;
box-shadow: 0 2px 9px 0 #ecebeb;
box-shadow: 0 2px 9px 0 #00000029;
// Trapezoid
transform: perspective(50px) rotateX(-10deg);
z-index: 1001;
}
.nav-course-operations
+84 -86
View File
@@ -1,57 +1,44 @@
import {Component, Prop, Vue} from 'vue-property-decorator';
import Course from '@/logic/course';
import Constants from '@/constants';
import {Course} from '@/components/app/app';
import {CourseUtils} from '@/utils/course-utils';
import {FormatUtils} from '@/utils/format-utils';
import LoginUser from '@/logic/login-user';
import NavController from '@/logic/nav-controller';
import App from '@/components/app/app';
/**
* This component is the top navigation bar
*/
@Component({
components: {},
})
@Component
export default class Navigation extends Vue
{
// @ts-ignore
@Prop() activeIndex: string;
@Prop({required: true}) nav: NavController;
@Prop({required: true}) courses: Course[];
@Prop({required: true}) user: LoginUser;
// @ts-ignore
@Prop() courses: Course[];
private gradingPeriod: string = 'All Year';
// Instance
public static instance: Navigation;
static instance: Navigation;
/**
* This is called when the instance is created.
*/
public created()
created()
{
// Set instance
Navigation.instance = this;
// Set history state
let url = window.location.pathname;
if (url == '/' || url == '') url = '/overall';
window.history.replaceState({lastTab: url.substring(1)}, '', url);
// Update initial index
this.updateIndex(url.substring(1), false);
// Create history state listener
window.onpopstate = e =>
// Check selected time
if (!this.$cookies.isKey('va.grading-period'))
{
if (e.state)
{
// Restore previous tab
console.log(`onPopState: Current: ${this.activeIndex}, Previous: ${e.state.lastTab}`);
this.updateIndex(e.state.lastTab, false);
}
};
this.$cookies.set('va.grading-period', this.gradingPeriod, '10y');
}
this.gradingPeriod = this.$cookies.get('va.grading-period');
}
public formatCourseIndex(course: Course)
/**
* This is called when the instance is loaded.
*/
mounted()
{
return CourseUtils.formatTabIndex(course);
Navigation.instance = this;
}
/**
@@ -60,55 +47,19 @@ export default class Navigation extends Vue
* @param index The index selected
* @param indexPath The path of the index
*/
public onSelect(index: string, indexPath: string)
onSelect(index: string, indexPath: string)
{
// Update active index
this.updateIndex(index);
}
/**
* Update index
*
* @param newIndex New index
* @param history Record in history or not (Default true)
*/
public updateIndex(newIndex: string, history?: boolean)
{
// Call custom event
this.$emit('update:activeIndex', newIndex);
// Record or not
if (history == null || history)
try
{
// Debug output TODO: Remove this
console.log(`onNavigate: Previous: ${this.activeIndex}, New: ${newIndex}`);
// Check url
let url = `/${newIndex}`;
// Push history state
window.history.pushState({lastTab: newIndex}, '', url);
// Is json
this.nav.updateIndex(JSON.parse(index))
}
// 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'))
catch (e)
{
return this.findCourse(index.split('/')[1], 0).name;
// Not json
this.nav.updateIndex(index);
}
// Others
return FormatUtils.toTitleCase(index);
}
/**
@@ -116,10 +67,10 @@ export default class Navigation extends Vue
*
* @param indexOffset Index offset (Eg. 1 for next)
*/
public nextCourse(indexOffset: number)
nextCourse(indexOffset: number)
{
// Set tab to the next index
this.updateIndex(CourseUtils.formatTabIndex(this.findNextCourse(indexOffset)))
this.nav.updateIndex(this.findNextCourse(indexOffset).urlIndex)
}
/**
@@ -127,9 +78,9 @@ export default class Navigation extends Vue
*
* @param indexOffset Index offset (Eg. 1 for next)
*/
public findNextCourse(indexOffset: number)
findNextCourse(indexOffset: number)
{
return this.findCourse(this.activeIndex.split('/')[1], indexOffset);
return this.findCourse(this.nav.info.id, indexOffset);
}
/**
@@ -138,7 +89,7 @@ export default class Navigation extends Vue
* @param courseId Course ID
* @param indexOffset Index offset (Eg. 1 for next)
*/
public findCourse(courseId: string, indexOffset: number)
findCourse(courseId: string, indexOffset: number)
{
// Find current course index
let courseIndex = this.courses.findIndex(c => c.id == +courseId);
@@ -148,11 +99,58 @@ export default class Navigation extends Vue
}
/**
* This function is called when the sign out button is clicked.
* Select grading period
*
* @param command Term 1, Term 2, All Year, etc.
*/
public signOut()
selectGradingPeriod(command: string)
{
// Call custom event
this.$emit('sign-out');
this.gradingPeriod = command;
this.$cookies.set('va.grading-period', command, '10y');
// Call event
this.$emit('select-time', this.getSelectedTerm());
}
/**
* Get code for selected time
*/
getSelectedTerm(): number
{
if (this.gradingPeriod == 'All Year') return -1;
else return +this.gradingPeriod.replace('Term ', '') - 1;
}
/**
* Avatar dropdown menu event
*
* @param cmd Command: sign-out
*/
onAvatarMenu(cmd: string)
{
switch (cmd)
{
case 'sign-out':
{
this.$emit('sign-out');
break
}
case 'switch-dark':
{
App.instance.darkMode = !App.instance.darkMode;
if (this.isDark()) this.$cookies.set('dark', true);
else this.$cookies.remove('dark');
break
}
}
}
isDark()
{
return App.instance.darkMode;
}
get version() {return Constants.VERSION}
}
+49 -14
View File
@@ -1,33 +1,68 @@
<template>
<div id="navigation">
<el-menu style="margin-bottom: 10px;" class="centered" mode="horizontal"
:default-active="activeIndex" @select="onSelect">
:default-active="nav.id" @select="onSelect">
<!--div id="nav-title">
Veracross Analyzer
</div-->
<div id="nav-title">
<img id="nav-logo" alt="logo" src="../../assets/logo.png">
<span id="nav-logo-text" class="logo-text">Veracross Analyzer</span>
<span id="nav-logo-version">v{{version}}</span>
</div>
<el-menu-item index="overall">Overall</el-menu-item>
<el-submenu index="courses">
<el-submenu index="">
<template slot="title">Courses</template>
<el-menu-item v-for="course in courses"
:index="formatCourseIndex(course)"
:key="course.name">{{course.name}}</el-menu-item>
:index="JSON.stringify(course.urlIndex)"
:key="course.id">{{course.name}}</el-menu-item>
</el-submenu>
<el-button @click="signOut" id="sign-out-button" type="text">Sign Out</el-button>
<!-- Grading period selection -->
<el-dropdown id="nav-grading-period" @command="selectGradingPeriod">
<el-button type="primary" size="medium">
{{gradingPeriod}}<i class="el-icon-arrow-down el-icon--right"/>
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="Term 1">Term 1</el-dropdown-item>
<el-dropdown-item command="Term 2">Term 2</el-dropdown-item>
<el-dropdown-item command="Term 3">Term 3</el-dropdown-item>
<el-dropdown-item command="Term 4" disabled>Term 4</el-dropdown-item>
<!-- TODO: Auto enable / disable quarters -->
<el-dropdown-item command="All Year" divided>All Year</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<!-- User avatar -->
<el-dropdown id="nav-avatar" trigger="click" @command="onAvatarMenu">
<el-avatar :src="user.avatarUrl"/>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item style="text-align: center">{{user.nickname}}</el-dropdown-item>
<el-dropdown-item icon="el-icon-sunrise" command="switch-dark" divided>{{!isDark() ? 'Dark Mode (Unfinished)' : 'Light Mode'}}</el-dropdown-item>
<el-dropdown-item icon="el-icon-switch-button" command="sign-out" divided>Sign Out</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</el-menu>
<div v-if="activeIndex.includes('course') && findNextCourse(-1) != null"
<!-- Previous course / Next course (Only when the page is courses) -->
<div v-if="nav.id === 'course' && findNextCourse(-1) != null"
@click="nextCourse(-1)" id="prev-course" class="nav-course-operations unselectable">
PREVIOUS COURSE
</div>
<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 v-if="nav.id === 'course' && findNextCourse(1) != null"
@click="nextCourse(1)" id="next-course" class="nav-course-operations unselectable">
NEXT COURSE
</div>
</footer>
<!-- Back to top -->
<el-backtop style="box-shadow: rgba(0, 0, 0, 0.23) 0 3px 11px 0;"/>
</div>
</template>
<script src="./navigation.ts" lang="ts"></script>
<style src="./navigation.scss" lang="scss"></style>
<style src="./navigation.scss" lang="scss"/>
@@ -1,7 +1,7 @@
<template>
<div id="loading">
<div id="text" :class="message()">
{{message()}}
<div id="text" :class="message">
{{message}}
<div v-if="!error" class="el-loading-spinner">
<svg viewBox="25 25 50 50" class="circular">
@@ -10,7 +10,7 @@
</div>
<div v-if="error" id="error-details">
<span v-for="(line, index) in getText()" :style="`font-size: ${-index === 0 ? 16 : 12}px;`">
<span v-for="(line, index) in split" :style="`font-size: ${-index === 0 ? 16 : 12}px;`">
{{line}}
<br>
</span>
@@ -18,7 +18,7 @@
</div>
<div v-if="!error" id="details">
<span v-for="(line, index) in getText()" :style="`font-size: ${16 - getText().length + index}px;`">
<span v-for="(line, index) in split" :style="`font-size: ${16 - split.length + index}px;`">
{{line}}
<br>
</span>
@@ -29,23 +29,19 @@
<script lang="ts">
import {Component, Prop, Vue} from 'vue-property-decorator';
@Component({
components: {}
})
@Component
export default class Loading extends Vue
{
// @ts-ignore
@Prop() text: string;
@Prop({required: true}) text: string;
// @ts-ignore
@Prop() error: boolean;
@Prop({required: true}) error: boolean;
getText()
get split()
{
return this.text.split('\n');
}
message()
get message()
{
return this.error ? 'Error' : 'Loading';
}
+63
View File
@@ -0,0 +1,63 @@
<template>
<div id="maintenance">
<div id="maintenance-content">
<h1>We&rsquo;ll be back soon!</h1>
<div>
<p>Sorry for the inconvenience but we&rsquo;re performing some maintenance at the moment.
We&rsquo;ll be back online shortly!</p>
<p>What went wrong: {{json.reason}}</p>
<p>Estimated fix: {{json.eta}}</p>
<p>&mdash; An Average SJP Junior</p>
</div>
</div>
</div>
</template>
<script lang="ts">
import {Component, Prop, Vue} from 'vue-property-decorator';
@Component
export default class Maintenance extends Vue
{
@Prop({required: true}) message: any;
get json()
{
return JSON.parse(this.message);
}
}
</script>
<style lang="scss" scoped>
#maintenance
{
z-index: 1000;
background: white;
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
#maintenance-content
{
font: 20px Helvetica, sans-serif;
color: #333;
display: block;
text-align: left;
margin: 150px;
h1
{
font-size: 50px;
}
a {color: #dc8100; text-decoration: none;}
a:hover {color: #333; text-decoration: none;}
}
}
</style>
+31 -7
View File
@@ -1,20 +1,23 @@
/**
* This class stores the static constants.
*/
import {findLastIndex} from '@/logic/utils/general-utils';
export default class Constants
{
/** Base url for api access */
public static API_URL: string = 'https://va.hydev.org/api';
static API_URL: string = 'https://va.hydev.org/api';
// static API_URL: string = 'http://localhost:24021/api';
/** Current version */
public static VERSION: string = '0.3.6.741';
static VERSION: string = '0.5.4.1391';
/** Minimum version that still supports the same cookies */
public static MIN_SUPPORTED_VERSION: string = '0.3.4.561';
/** The minimum version that still supports the same cookies */
static MIN_SUPPORTED_VERSION: string = '0.4.6.1087';
public static GITHUB: string = 'https://github.com/HyDevelop/VeracrossAnalyzer.Client';
static GITHUB: string = 'https://github.com/HyDevelop/VeracrossAnalyzer.Client';
public static SPLASH: string =
static SPLASH: string =
'. , ,---. | \n' +
'| |. , |---|,---.,---.| , .,---,,---.,---.\n' +
' \\ / >< | || |,---|| | | .-\' |---\'| \n' +
@@ -24,7 +27,7 @@ export default class Constants
` Github: ${Constants.GITHUB}`;
// Graph Theme
public static THEME =
static THEME =
{
// Colors
colors:
@@ -53,5 +56,26 @@ export default class Constants
'#724e58',
'#4b565b'
]
};
// Terms (TODO: Actually get the terms dynamically
static TERMS =
[
new Date('Sep 04 2019'),
new Date('Nov 03 2019'),
new Date('Jan 19 2020'),
new Date('Mar 22 2020'),
new Date('Jun 05 2020'),
];
static CURRENT_TERM = Constants.getTerm(new Date());
/**
* Find out the specified date is in which term
*
* @param date
*/
static getTerm(date: Date)
{
return findLastIndex(Constants.TERMS, d => d <= date);
}
}
+423
View File
@@ -0,0 +1,423 @@
import {FormatUtils} from '@/logic/utils/format-utils';
import {CourseUtils} from '@/logic/utils/course-utils';
import Navigation from '@/components/navigation/navigation';
import {GPAUtils} from '@/logic/utils/gpa-utils';
import CacheUtils from '@/logic/utils/cache-utils';
import Constants from '@/constants';
import {Index} from '@/logic/nav-controller';
import App from '@/components/app/app';
/**
* Objects of this interface represent assignment grades.
*/
export class Assignment
{
id: number;
scoreId: number;
type: string;
typeId: number;
description: string;
time: number;
complete: string;
include: boolean;
display: boolean;
unread: boolean;
scoreMax: number;
score: number;
gradingPeriod: number;
// Callbacks when this object updates
private updateCallbacks: (() => void)[] = [];
/**
* Construct assignment with json object
*
* @param json Json object
*/
constructor(json: any)
{
this.id = json.assignment_id;
this.scoreId = json.score_id;
this.type = json.assignment_type;
this.typeId = json.assignment_type_id;
this.description = json.assignment_description;
this.time = new Date(json._date).getTime();
this.complete = json.completion_status;
this.include = json.include_in_calculated_grade == 1;
this.display = json.display_grade == 1;
this.unread = json.is_unread == 1;
this.scoreMax = json.maximum_score;
this.score = +json.raw_score;
this.gradingPeriod = +json.grading_period.replace('Quarter ', '') - 1;
}
/**
* Graded or not
*/
get graded()
{
// TODO: Add more cases
// Incomplete doesn't mean that the teacher didn't grade it yet, which is "Pending".
// NREQ is not graded.
return this.include && (this.complete == 'Complete' || this.complete == 'Late' || this.complete == 'Incomplete');
}
/**
* What is the problem with this assignment
*
* @return string Empty string if complete, otherwise return problem.
*/
get problem()
{
switch (this.complete)
{
case 'Pending': return 'Pending'; // ID: 0
case 'Incomplete': return 'Incomplete'; // ID: 2
case 'Complete': return ''; // ID: 3
case 'NREQ': return 'Dropped'; // ID: 4
case 'Late': return 'Late';
}
}
/**
* Get the text color of the problem
*/
get problemColor()
{
switch (this.complete)
{
case 'Pending': return '#b1b1b1';
case 'Incomplete': return '#ff7a2f';
case 'NREQ': return '#41b141';
case 'Late': return '#ff0036';
}
}
/**
* Add callback
*
* @param callback
*/
addCallback(callback: () => void)
{
this.updateCallbacks.push(callback);
}
/**
* Mark as read
*/
markAsRead(): Promise<void>
{
return new Promise((resolve, reject) => {
App.http.post('/mark-as-read', {scoreId: this.scoreId})
.then(response =>
{
// Check success
if (response.success)
{
this.unread = false;
this.updateCallbacks.forEach(callback => callback());
resolve();
}
else reject(response.data);
})
.catch(reject)
})
}
}
export interface AssignmentType
{
id: number
name: string
weight: number
scoreMax: number
score: number
percent: number
assignmentCount: number
}
export interface Grading
{
method: string
weightingMap: {[index: string]: number}
}
export default class Course
{
id: number;
assignmentsId: number;
name: string;
teacherName: string;
status: string;
rawAssignments: Assignment[];
rawLetterGrade?: string;
rawNumericGrade?: number;
level: string;
scaleUp: number;
termGrading: Grading[];
termAssignments: Assignment[][];
cache: CacheUtils = new CacheUtils();
/**
* Construct a course with a course json object
*
* @param courseJson Course json object
*/
constructor(courseJson: any)
{
this.id = courseJson.id;
this.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.termGrading = new Array(4).fill(null);
}
/**
* Load in assignments data
*
* @param data Assignments data
*/
loadAssignments(data: any)
{
// Load assignments
// Parse json and filter it
this.rawAssignments = data.assignments.map((a: any) => new Assignment(a));
// Sort by date (Latest is at 0)
this.rawAssignments.sort((a, b) => b.time - a.time);
// Filter assignments into terms
this.termAssignments = [[], [], [], []];
// Loop through it by time order
this.rawAssignments.forEach(a => this.termAssignments[a.gradingPeriod].push(a));
}
/**
* Is graded or not
*/
get isGraded(): boolean
{
// Skip future or past courses
if (this.status != 'active') return false;
// Skip courses without levels TODO: Ask for user input
if (this.level == 'None' || this.level == 'Unknown' || this.scaleUp == -1) return false;
// Skip courses without graded assignments
if (this.assignments.length == 0) return false;
// Skip if there are no grading scale
// if (course.grading.method == 'NOT_GRADED') return;
// Is graded
return true;
}
/**
* Get currently selected grading periods
*/
get gradingPeriods(): number[]
{
return this.cache.get('GradingPeriods', () =>
{
return (this.rawSelectedTerm == -1 ? [0, 1, 2, 3] : [this.rawSelectedTerm]).filter(term =>
this.termAssignments[term].filter(a => a.graded).length != 0);
})
}
/**
* Get currently selected grading periods
*/
get allGradingPeriods(): number[]
{
return this.cache.get('AllGradingPeriods', () =>
{
return [0, 1, 2, 3].filter(term => this.termAssignments[term].filter(a => a.graded).length != 0);
})
}
/**
* Get assignments of the selected grading periods
*/
get assignments(): Assignment[]
{
return this.gradingPeriods
.flatMap(term => this.termAssignments[term])
.sort((a, b) => b.time - a.time);
}
/**
* Get assignments before a certain date
*
* @param time
*/
getAssignmentsBefore(time: number): {term: number, assignments: Assignment[]}
{
let term = Constants.getTerm(new Date(time));
let assignments = this.assignments.filter(a => a.gradingPeriod == term && a.time <= time);
return {term: term, assignments: assignments}
}
/**
* Get letter grade
*/
get letterGrade(): string
{
return this.cache.get('LetterGrade', () =>
{
// Get scale
let scale = GPAUtils.findScale(this.numericGrade);
// Scale not found
return scale == undefined ? '--' : scale.letter;
})
}
/**
* Get letter grade by term
*
* @param term
*/
letterGradeTerm(term: number): string
{
return this.cache.get('LetterGrade' + term, () =>
{
// Get scale
let scale = GPAUtils.findScale(this.numericGradeTerm(term));
// Scale not found
return scale == undefined ? '--' : scale.letter;
})
}
/**
* Get numeric grade
*/
get numericGrade()
{
return this.cache.get('NumericGrade', () =>
{
return this.gradingPeriods.map(term => this.numericGradeTerm(term))
.reduce((p, v) => p + v) / this.gradingPeriods.length
})
}
/**
* Get numeric grade by term
*
* @param term
*/
numericGradeTerm(term: number): number
{
return this.cache.get('NumericGrade' + term, () =>
{
// Calculate
if (this.termGrading[term].method == 'PERCENT_TYPE')
{
return GPAUtils.getPercentTypeAverage(this.termGrading[term], this.termAssignments[term]);
}
else if (this.termGrading[term].method == 'TOTAL_MEAN')
{
return GPAUtils.getTotalMeanAverage(this.termAssignments[term]);
}
else return -1;
})
}
/**
* Get assignment types
*/
get assignmentTypes(): AssignmentType[]
{
return this.cache.get('AssignmentTypes', () =>
{
// Get all types
let types = this.assignments.map(a => a.type);
// Remove duplicates
types = types.filter((type, i, a) => a.indexOf(type) == i);
// Get total possible score for weight calculation
let totalScoreMax = this.assignments.reduce((sum, a) => sum + a.scoreMax, 0);
// For every type...
return types.map(type =>
{
// Get assignments of the type
let typeAssignments = this.assignments.filter(a => a.graded && a.type == type);
// Count scores and max scores
let score = typeAssignments.reduce((sum, a) => sum + a.score, 0);
let scoreMax = typeAssignments.reduce((sum, a) => sum + a.scoreMax, 0);
// Calculate weight
let weight = this.termGrading[0].method == 'PERCENT_TYPE'
? this.termGrading[0].weightingMap[type] : scoreMax / totalScoreMax;
// Return
return {name: type, id: typeAssignments[0].typeId, weight: +(weight * 100).toFixed(2),
scoreMax: scoreMax, score: score, percent: +(score / scoreMax * 100).toFixed(2),
assignmentCount: typeAssignments.length}
})
})
}
/**
* Get url hash code
*/
get urlHash(): string
{
return `course/${this.id}`
}
/**
* Get navigation index
*/
get urlIndex(): Index
{
return {hash: this.urlHash, title: this.name, identifier: 'course', info: {id: this.id}}
}
/**
* Selected term
*/
get rawSelectedTerm(): number
{
return Navigation.instance.getSelectedTerm()
}
}
+46
View File
@@ -0,0 +1,46 @@
const md5 = require('md5');
export default class LoginUser
{
id: number;
schoolPersonPk: number;
username: string;
lastLogin: Date;
firstLogin: Date;
firstName: string;
lastName: string;
nickname: string;
graduationYear: string;
groups: string;
emails: string[];
classes: string[];
birthday: string;
avatarUrl: string;
token: string;
constructor(json: any)
{
this.id = json.id;
this.schoolPersonPk = json.schoolPersonPk;
this.username = json.username;
this.lastLogin = new Date(json.lastLogin);
this.firstLogin = new Date(json.firstLogin);
this.firstName = json.firstName;
this.lastName = json.lastName;
this.nickname = json.nickname;
this.graduationYear = json.graduationYear;
this.groups = json.groups;
this.emails = json.emails.split('|').map((e: any) => e.toLowerCase().trim());
this.classes = json.classes.split('|');
this.birthday = json.birthday;
this.avatarUrl = json.avatarUrl;
this.token = json.token;
// Generate default avatar
if (this.avatarUrl == null || this.avatarUrl == '')
{
this.avatarUrl = `https://www.gravatar.com/avatar/${md5(this.emails[0])}?d=404` + encodeURIComponent(
`https://ui-avatars.com/api/${this.firstName.charAt(0)}${this.lastName.charAt(0)}/128`);
}
}
}
+160
View File
@@ -0,0 +1,160 @@
import {FormatUtils} from '@/logic/utils/format-utils';
import pWaitFor from 'p-wait-for';
import App from '@/components/app/app';
export interface Index
{
hash: string
title?: string
identifier: string
info?: any
}
export default class NavController
{
// Current index
index: Index;
// Callback
updateCallback?: () => void;
constructor()
{
// Create history state listener
window.onpopstate = (e: any) =>
{
if (e.state)
{
// Restore previous tab
//console.log(`onPopState: Current: ${this.index.hash}, Previous: ${e.state.hash}`);
this.updateIndex(e.state, false);
}
};
// Initialize
this.init()
}
/**
* Initialize from last location
*/
private init()
{
if (window.location.hash == '#info') return;
// Check history from last session
if (window.history.state != undefined && window.history.state.hash != undefined)
{
// Last history exists
this.index = window.history.state;
return;
}
// Last history doesn't exist but hash url might exist
let hash = window.location.hash.replace('#', '');
// Check hash
if (hash == '')
{
// No location info in url, set page to overall
window.history.replaceState(this.convertIndex('overall'), '', '/#overall');
this.updateIndex('overall', false);
return;
}
// There is hash info in url
let split = hash.split('/');
// Not course -> don't know what to do with this url, so just refresh
if (split[0] != 'course')
{
this.initClear();
return;
}
// Is course -> Update index with placeholder title
this.updateIndex({hash: hash, title: `Loading...`, identifier: 'course', info: {id: +split[1]}}, false);
// Wait for courses to finish loading
pWaitFor(() => App.instance != undefined && App.instance.assignmentsReady).then(() =>
{
// Find course
let course = App.instance.courses.find(c => c.id == +split[1]);
// This person has no such course, refresh to overall
if (course == null)
{
this.initClear();
return;
}
window.history.replaceState(course.urlIndex, '', '/#' + course.urlHash);
this.updateIndex(course.urlIndex, false);
})
}
private initClear()
{
window.location.hash = '';
window.location.reload();
}
/**
* Update index
*
* @param index Hash and title | Hash only
* @param history Record in history or not (Default true)
*/
updateIndex(index: Index | string, history: boolean = true)
{
index = this.convertIndex(index);
// Call custom event
if (this.updateCallback != null) this.updateCallback();
// Record history or not
if (history)
{
//console.log(`history: Current: ${this.index.hash}, New: ${index.hash}`);
// Check url
let url = `/#${index.hash}`;
// Push history state
window.history.pushState(index, '', url);
}
// Update title
document.title = 'Veracross Analyzer - ' + index.title;
// Scroll to top
window.scrollTo(0, 0);
// Update selected index
this.index = index;
}
/**
* Check index conversion
*
* @param index Hash and title | Hash only
* @return Index Hash and title
*/
private convertIndex(index: Index | string): Index
{
// Convert index format if it is hash only
if (typeof index == 'string') index = {hash: index, identifier: index};
if (index.title == null) index.title = FormatUtils.toTitleCase(index.hash);
return index;
}
get id(): string
{
return this.index.identifier
}
get info(): any
{
return this.index.info
}
}
+20
View File
@@ -0,0 +1,20 @@
export default class CacheUtils
{
map: Map<string, any> = new Map();
/**
* Get a cached value, or if not cached, cache it.
*
* @param name Name of the cached value
* @param callback Callback function
*/
public get(name: string, callback: () => any)
{
if (!this.map.has(name))
{
this.map.set(name, callback());
}
return this.map.get(name);
}
}
+77
View File
@@ -0,0 +1,77 @@
import Navigation from '@/components/navigation/navigation';
import Constants from '@/constants';
const LEVEL_AP = {level: 'AP', scaleUp: 1};
const LEVEL_H = {level: 'H', scaleUp: 0.75};
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
{
/**
* 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;
}
/**
* Get the begin date of the selected term
*/
static getTermBeginDate()
{
let selected = Navigation.instance.getSelectedTerm();
return selected == -1 ? Constants.TERMS[0] : Constants.TERMS[selected];
}
/**
* Get the end date of the selected term
*/
static getTermEndDate()
{
let selected = Navigation.instance.getSelectedTerm();
return selected == -1 ? Constants.TERMS[4] : Constants.TERMS[selected + 1];
}
}
@@ -1,21 +1,5 @@
import moment from 'moment';
export class FormatUtils
{
/**
* Convert date format to yyyy-mm-dd
*
* @param _date Date
*/
public static toChartDate(_date: string | Date)
{
// Convert to Date
let date: Date = _date instanceof Date ? _date : new Date(_date);
// Convert to yyyy-mm-dd
return moment(date).format('YYYY-MM-DD');
}
/**
* Limit string length
*
@@ -37,4 +21,14 @@ export class FormatUtils
{
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(/&amp;/g, '&');
}
}
+7
View File
@@ -0,0 +1,7 @@
export function findLastIndex<T>(array: T[], callback: (v: T) => boolean): number
{
let arr2 = array.slice().reverse();
let result = arr2.findIndex(callback);
return result == -1 ? -1 : arr2.length - result - 1;
}
+189
View File
@@ -0,0 +1,189 @@
import Course, {Assignment, Grading} from '@/logic/course';
export interface Scale
{
min: number
letter: string
gp: number
}
/**
* This is an utility class to calculate GPA.
*/
export class GPAUtils
{
// [[Min score, Letter grade, Base GPA], ...]
public static SCALE: Scale[] =
[
{min: 96.5, letter: 'A+', gp: 4.00},
{min: 92.5, letter: 'A' , gp: 3.75},
{min: 89.5, letter: 'A-', gp: 3.50},
{min: 86.5, letter: 'B+', gp: 3.25},
{min: 82.5, letter: 'B' , gp: 3.00},
{min: 79.5, letter: 'B-', gp: 2.75},
{min: 76.5, letter: 'C+', gp: 2.50},
{min: 72.5, letter: 'C' , gp: 2.25},
{min: 70.5, letter: 'C-', gp: 2.00},
{min: 69.5, letter: 'D' , gp: 1.00},
{min: 0 , letter: 'F' , gp: 0.00}
];
/**
* Calculate GPA for a list of couses
*
* @param coursesOriginal List of courses
*/
public static getGPA(coursesOriginal: Course[]): {gpa: number, accurate: boolean, max: number}
{
// Clone array
let courses: Course[] = [];
// Accurate or not
let accurate: boolean = true;
// Remove all courses that does not have a grade
coursesOriginal.forEach(course =>
{
if (course.letterGrade == null || course.letterGrade == '')
{
accurate = false;
}
else if (course.level != 'none' && !isNaN(course.numericGrade))
{
courses.push(course);
}
});
// If no course have grade, return -1
if (courses.length == 0)
{
return {gpa: -1, accurate: false, max: -1};
}
// Count total GPA
let totalGPA = 0;
let maxTotal = 0;
courses.forEach(course =>
{
totalGPA += this.getGP(course, course.numericGrade);
maxTotal += this.getGP(course, 'A+');
});
// Get average GPA, round to two decimal places
let gpa = Math.round(totalGPA / courses.length * 100) / 100;
let maxGPA = Math.round(maxTotal / courses.length * 100) / 100;
// Return results
return {gpa: gpa, accurate: accurate, max: maxGPA};
}
/**
* Calculate GPA for a course
*
* @param course Course
* @param letterGrade Letter grade
*/
public static getGP(course: Course, letterGrade: string | number): number
{
// Get scale
let scale = this.findScale(letterGrade);
// No scale
if (scale == undefined) return -1;
// Add scaleUp if not failed.
return scale.gp == 0 ? 0 : scale.gp + course.scaleUp;
}
/**
* Find the scale for a grade
*
* @param grade Letter grade or numeric grade
*/
public static findScale(grade: string | number): Scale | undefined
{
// Letter grade
if (typeof grade == 'string')
{
return this.SCALE.find(scale => scale.letter == grade);
}
// Numeric grade
return this.SCALE.find(scale => grade >= scale.min);
}
/**
* Calculate the total-mean (total/max) average
*
* @param assignments
*/
public static getTotalMeanAverage(assignments: Assignment[])
{
let score = 0;
let max = 0;
// Loop through assignments
assignments.forEach(assignment =>
{
// If assignment should be displayed
if (!assignment.graded) return;
// Record scores
score += assignment.score;
max += assignment.scoreMax;
});
// Return
return +(score / max * 100).toFixed(2);
}
/**
* Calculate the percent type
*
* @param grading
* @param assignments
*/
public static getPercentTypeAverage(grading: Grading, assignments: Assignment[])
{
let typeScores: {[index: string]: any} = {};
let typeCounts: {[index: string]: any} = {};
// Loop through assignments
assignments.forEach(assignment =>
{
// If assignment should be displayed
if (!assignment.graded) return;
// Record scores
if (typeScores[assignment.type] == undefined) typeScores[assignment.type] = 0;
typeScores[assignment.type] += assignment.score / assignment.scoreMax;
if (typeCounts[assignment.type] == undefined) typeCounts[assignment.type] = 0;
typeCounts[assignment.type] ++;
});
// Count total percentage (This is to avoid less than expected cases)
// Eg. If HW = 25% and Quiz = 75%, I have 1 hw and 0 quiz
// Without total percentage, the avg grade I get is 25%.
let totalPercentage = 0;
for (let type in grading.weightingMap)
{
if (typeScores[type] != undefined)
{
totalPercentage += grading.weightingMap[type];
}
}
// Count
let score = 0;
for (let type in typeScores)
{
let typeFactor = grading.weightingMap[type] / totalPercentage;
score += typeScores[type] * typeFactor / typeCounts[type];
}
// Add average to the row
return +(score * 100).toFixed(2);
}
}
+103
View File
@@ -0,0 +1,103 @@
import Constants from '@/constants';
import App from '@/components/app/app';
export default class GraphUtils
{
static DOT = '<span style="display:inline-block;margin-right:5px;border-radius:10px;width:9px;height:9px;background-color:{color}"></span>';
/**
* Base settings
*
* @param title
* @param subtitle
*/
static getBaseSettings(title?: String, subtitle?: String)
{
return {
// Color
color: Constants.THEME.colors,
backgroundColor: 'transparent',
// Title
title:
{
show: title != null,
textStyle:
{
fontSize: 13
},
text: title,
subtext: subtitle,
x: 'center'
},
}
}
/**
* Get term mark lines
*/
static getTermLines()
{
return {
silent: true,
symbol: 'none',
lineStyle: {color: Constants.THEME.colors[2]},
animationDuration: 500,
data: Constants.TERMS.map((term, index) =>
{
return {xAxis: term.getTime(), label: {formatter: `Term ${index + 1}`}}
})
}
}
/**
* Get mark areas for percentage scores
*/
static getGradeMarkAreas(opacity: number)
{
// TODO: Auto update after switching dark mode (possibly by refreshing)
opacity = App.instance.darkMode ? 0.1 : opacity;
return {
silent: true,
data:
[
// Above 100
[{itemStyle: {color: 'rgba(230,253,255)', opacity: opacity}, yAxis: 120}, {yAxis: 100}],
// 90 to 100
[{itemStyle: {color: 'rgba(241,255,237)', opacity: opacity}, yAxis: 100}, {yAxis: 90}],
// 80 to 90
[{itemStyle: {color: 'rgba(255,250,216)', opacity: opacity}, yAxis: 90}, {yAxis: 80}],
// 70 to 80
[{itemStyle: {color: 'rgba(255,225,199)', opacity: opacity}, yAxis: 80}, {yAxis: 70}],
// Below 70 (Fail)
[{itemStyle: {color: 'rgb(255,190,184)', opacity: opacity}, yAxis: 70}, {yAxis: -100}]
]
}
}
/**
* Text style for pie graphs or radar graphs
*/
static pieTextStyle()
{
return {
fontSize: 14,
textShadowColor: '#cfcfcf',
textShadowBlur: 2,
textShadowOffsetX: 1,
textShadowOffsetY: 1,
backgroundColor: '#f6f6f6',
borderRadius: 3,
padding: [3, 5]
}
}
/**
* CSS shadow string (extraCssText) for tooltip
*/
static tooltipCssShadow()
{
return {extraCssText: 'box-shadow: 0 0 3px rgba(0, 0, 0, 0.3);'}
}
}
@@ -1,13 +1,14 @@
import Constants from '@/constants';
import LoginUser from '@/logic/login-user';
export class HttpUtils
{
public token: string = '';
public user: LoginUser;
public post(node: string, body: any): Promise<any>
{
// Add token
if (this.token != '') body['token'] = this.token;
if (this.user != null) body['token'] = this.user.token;
// Create promise
return new Promise<any>((resolve, reject) =>
+2 -2
View File
@@ -1,10 +1,10 @@
import Vue from 'vue';
import ElementUI from 'element-ui';
const VCharts = require('v-charts');
import App from './components/app/app.vue';
import VueCookies from 'vue-cookies';
const VCharts = require('v-charts');
Vue.config.productionTip = false;
// Use Element UI
@@ -0,0 +1,79 @@
<template>
<div id="assignment-type-head">
<el-card :body-style="{padding: '0px'}">
<div id="type-info-card">
<span id="type-name">{{type.name}}</span>
<span id="type-average">Average: {{type.percent}}%</span>
</div>
<AssignmentEntry v-for="(assignment, index) of filteredAssignments" :key="assignment.id"
:assignment="assignment" :unread="false"
:backgroundColor="index % 2 === 1 ? '#ffffff' : '#f7f7f7'" narrow="true">
</AssignmentEntry>
</el-card>
</div>
</template>
<script lang="ts">
import {Component, Prop, Vue} from 'vue-property-decorator';
import AssignmentEntry from '@/pages/overall/overall-course/assignment-entry/assignment-entry.vue';
import {Assignment, AssignmentType} from '@/logic/course';
@Component({
components: {AssignmentEntry}
})
export default class AssignmentTypeHead extends Vue
{
@Prop({required: true}) type: AssignmentType;
@Prop({required: true}) assignments: Assignment[];
get filteredAssignments()
{
// Filter assignments to only this type
return this.assignments.filter(a => a.typeId == this.type.id);
}
}
</script>
<style lang="scss" scoped>
#assignment-type-head
{
margin-bottom: 20px;
}
#type-info-card
{
height: 60px;
}
#type-name
{
// Font
font-size: 22px;
color: var(--main);
// Center
height: 60px;
line-height: 60px;
// Alignment
padding-left: 20px;
float: left;
}
#type-average
{
// Font
font-size: 14px;
color: #8db3e4;
// Center
height: 60px;
line-height: 64px;
// Alignment
float: left;
margin-left: 15px;
display: inline-block;
}
</style>
+6
View File
@@ -18,3 +18,9 @@
// https://stackoverflow.com/questions/17572619/inset-box-shadow-only-on-one-side
box-shadow: inset 0 7px 9px -7px rgba(0,0,0,0.1);
}
.type-graph
{
padding-top: 23px;
height: 420px !important;
}
-29
View File
@@ -1,29 +0,0 @@
import {Component, Prop, Vue} from 'vue-property-decorator';
import {Assignment, Course} 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';
@Component({
components: {CourseHead, CourseScatter}
})
export default class CoursePage extends Vue
{
// @ts-ignore
@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;
}
}
+56 -6
View File
@@ -1,21 +1,71 @@
<template>
<el-card id="course-card" class="course-card">
<course-head :course="course" :unread="countUnread()"></course-head>
<course-head :clickable="false" :course="course" :unread="countUnread()"/>
<div class="course-card-content expand">
<el-row>
<el-col :span="24">
<el-col :span="24" class="course-page-graph">
<el-card class="large overall-line-card vertical-center">
<course-scatter :course="course"></course-scatter>
<course-scatter :course="course"/>
</el-card>
</el-col>
<el-col :span="0">
</el-row>
<el-row>
<el-col :span="12" class="course-page-graph">
<el-card class="large overall-line-card vertical-center type-graph"
body-style="padding: 0">
<TypeRadar :course="course"/>
</el-card>
</el-col>
<el-col :span="12" class="course-page-graph">
<el-card class="large overall-line-card vertical-center type-graph"
body-style="padding: 0">
<TypePie :course="course"/>
</el-card>
</el-col>
</el-row>
<AssignmentTypeHead v-for="type in course.assignmentTypes" :key="type.id"
:type="type" :assignments="course.assignments">
</AssignmentTypeHead>
</div>
</el-card>
</template>
<script src="./course-page.ts" lang="ts"></script>
<style src="./course-page.scss" lang="scss" scoped></style>
<script lang="ts">
import {Component, Prop, Vue} from 'vue-property-decorator';
import CourseHead from '@/pages/overall/overall-course/course-head/course-head.vue';
import CourseScatter from '@/pages/course/course-scatter/course-scatter';
import AssignmentEntry from '@/pages/overall/overall-course/assignment-entry/assignment-entry.vue';
import AssignmentTypeHead from '@/pages/course/assignment-type-head/assignment-type-head.vue';
import Course, {Assignment} from '@/logic/course';
import TypeRadar from '@/pages/course/type-radar/type-radar';
import TypePie from '@/pages/course/type-pie/type-pie';
@Component({
components: {TypeRadar, TypePie, AssignmentEntry, CourseHead, CourseScatter, AssignmentTypeHead}
})
export default class CoursePage extends Vue
{
@Prop({required: true}) course: Course;
private unread: number = -1;
private unreadAssignments: Assignment[] = [];
/**
* Count the number of unread assignments with cache
*/
countUnread(): number
{
if (this.unread == -1)
{
this.unreadAssignments = this.course.assignments.filter(a => a.unread);
return this.unread = this.unreadAssignments.length;
}
else return this.unread;
}
}
</script>
<style src="./course-page.scss" lang="scss" scoped/>
@@ -1,17 +1,14 @@
import {Component, Prop, Vue} from 'vue-property-decorator';
import {Assignment, Course} from '@/components/app/app';
import {GPAUtils} from '@/utils/gpa-utils';
import Constants from '@/constants';
import {FormatUtils} from '@/utils/format-utils';
import {FormatUtils} from '@/logic/utils/format-utils';
import moment from 'moment';
import Course, {Assignment} from '@/logic/course';
import GraphUtils from '@/logic/utils/graph-utils';
import chroma from 'chroma-js';
@Component({
})
@Component
export default class CourseScatter extends Vue
{
private static DOT = '<span style="display:inline-block;margin-right:5px;border-radius:10px;width:9px;height:9px;background-color:{color}"></span>';
// @ts-ignore
@Prop({required: true}) course: Course;
/**
@@ -29,40 +26,11 @@ export default class CourseScatter extends Vue
*/
get chartSettings()
{
// Map assignments
let map = this.mapAssignments();
// Scatter data point style
let itemStyle =
{
normal:
{
opacity: 0.8,
shadowBlur: 10,
shadowOffsetX: 0,
shadowOffsetY: 0,
shadowColor: 'rgba(0, 0, 0, 0.2)'
}
};
// Create settings
let settings =
{
// Color
color: Constants.THEME.colors,
// Title
title:
{
show: true,
textStyle:
{
fontSize: 13
},
text: 'Assignments',
subtext: 'Assignment scores for ' + this.course.name,
x: 'center'
},
// Base settings
...GraphUtils.getBaseSettings('Assignments', 'Assignment scores for ' + this.course.name),
// X axis represents course names
xAxis:
@@ -72,7 +40,7 @@ export default class CourseScatter extends Vue
{
formatter: (name: any) => moment(name).format('MMM DD')
},
max: FormatUtils.toChartDate(new Date())
max: new Date().getTime()
},
// Y axis represents GPAs and MaxGPAs
@@ -86,21 +54,23 @@ export default class CourseScatter extends Vue
{
formatter: (name: any) => name + '%'
},
max: 100,
min: (value: any) => Math.floor(value.min) - 5
min: (value: any) => Math.floor(value.min) - 5,
max: (value: any) => Math.min(Math.ceil(value.max), 110)
},
// Tooltip
tooltip:
{
...GraphUtils.tooltipCssShadow(),
trigger: 'axis',
axisPointer:
{
type: 'cross'
},
formatter: (ps: any[]) => ps[0].data[0] + '<br>' + ps.map(p =>
`${CourseScatter.DOT.replace('{color}', p.color)}
${FormatUtils.limit(p.data[2], 22)}: ${p.data[1]}%<br>`).join('')
formatter: (ps: any[]) => moment(ps[0].data[0]).format('MMM DD, YYYY') + '<br>' + ps.map(p =>
`${GraphUtils.DOT.replace('{color}', p.color.colorStops[1].color)}
${FormatUtils.limit(p.data[2].description, 22)}: ${p.data[1]}%<br>`).join('')
},
// Legend
@@ -116,39 +86,70 @@ export default class CourseScatter extends Vue
},
// Data
series: Array.from(map, ([type, assignments]) =>
{
return {
type: 'scatter',
name: type,
data: CourseScatter.assignmentsData(assignments),
itemStyle: itemStyle
}
})
series: this.series()
};
return settings;
}
/**
* Map assignments to {assignmentType, [assignment]} format.
* Get series data
*/
private mapAssignments(): Map<string, Assignment[]>
private series()
{
// Define map
let map = new Map();
// Move data to map
this.course.assignments.forEach(a =>
// Create scatter plots
let series: any[] = this.course.assignmentTypes.map((type, i) =>
{
// Null case, create empty array
if (!map.has(a.type)) map.set(a.type, []);
return {
type: 'scatter',
name: type.name,
data: CourseScatter.assignmentsData(this.course.assignments.filter(a => a.typeId == type.id)),
symbolSize: (data: any) => Math.max(Math.sqrt(type.weight * data[2].scoreMax / type.scoreMax) * 12, 12),
// Put data
map.get(a.type).push(a);
label:
{
emphasis:
{
show: true,
formatter: (p: any) => p.data[2].description,
position: 'top'
}
},
itemStyle:
{
normal:
{
opacity: 0.7,
shadowBlur: 10,
shadowOffsetX: 0,
shadowOffsetY: 0,
shadowColor: 'rgba(0, 0, 0, 0.2)',
color:
{
type: 'radial',
x: 0.4,
y: 0.3,
colorStops:
[
{offset: 0, color: chroma(Constants.THEME.colors[i]).set('hsl.l', 0.9).css()},
{offset: 1, color: Constants.THEME.colors[i]}
]
}
}
}
}
});
return map;
// Push other stuff
series.push(
{
type: 'line',
markLine: GraphUtils.getTermLines(),
markArea: GraphUtils.getGradeMarkAreas(0.4)
});
return series;
}
/**
@@ -159,6 +160,6 @@ export default class CourseScatter extends Vue
private static assignmentsData(assignments: Assignment[])
{
return assignments.filter(a => a.complete == 'Complete')
.map(a => [FormatUtils.toChartDate(a.date), (a.score / a.scoreMax * 100).toFixed(2), a.description]);
.map(a => [a.time, (a.score / a.scoreMax * 100).toFixed(2), a]);
}
}
@@ -1,16 +1,7 @@
<template>
<div id="course-scatter">
<ve-scatter height="450px" class="graph" :extend="{heyIUsedCourseObject: this.course.name}" :after-config="afterConfig"></ve-scatter>
<ve-scatter height="450px" class="graph" :extend="{a: this.course.name}" :after-config="afterConfig"/>
</div>
</template>
<script src="./course-scatter.ts" lang="ts"></script>
<style lang="scss" scoped>
#overall-bar
{
.graph
{
margin-top: 50px;
}
}
</style>
+56
View File
@@ -0,0 +1,56 @@
import {Component, Prop, Vue} from 'vue-property-decorator';
import Constants from '@/constants';
import Course from '@/logic/course';
import GraphUtils from '@/logic/utils/graph-utils';
@Component
export default class TypePie extends Vue
{
@Prop({required: true}) course: Course;
/**
* Override options
*
* @param options Original options (Unused)
*/
afterConfig(options: any)
{
return this.chartSettings;
}
/**
* Generate settings
*/
get chartSettings()
{
// Create settings
let settings =
{
...GraphUtils.getBaseSettings('Assignment Type Weight',
'How much each type of assignment affect your average'),
// Data
series:
{
type: 'pie',
avoidLabelOverlap: false,
radius: ['40%', '60%'],
center: ['50%', '55%'],
label: GraphUtils.pieTextStyle(),
data: this.course.assignmentTypes.map((t, i) => {return {
value: t.weight,
name: `${t.name}\n${t.weight}%`,
itemStyle:
{
color: Constants.THEME.colors[i],
opacity: 0.8,
shadowColor: 'rgba(0,0,0,0.22)',
shadowBlur: 10
}
}}).sort((a, b) => a.value - b.value)
}
};
return settings;
}
}
+8
View File
@@ -0,0 +1,8 @@
<template>
<div id="type-pie">
<ve-pie height="420px" class="graph" :extend="{a: this.course.name}" :after-config="afterConfig"></ve-pie>
</div>
</template>
<script src="./type-pie.ts" lang="ts"></script>
<style lang="scss" scoped></style>
+102
View File
@@ -0,0 +1,102 @@
import {Component, Prop, Vue} from 'vue-property-decorator';
import Constants from '@/constants';
import Course from '@/logic/course';
import GraphUtils from '@/logic/utils/graph-utils';
@Component
export default class TypeRadar extends Vue
{
@Prop({required: true}) course: Course;
/**
* Override options
*
* @param options Original options (Unused)
*/
afterConfig(options: any)
{
return this.chartSettings;
}
/**
* Generate settings
*/
get chartSettings()
{
let min = this.course.assignmentTypes.reduce((min, t) => Math.min(min, t.percent), 100);
// Create settings
let settings =
{
...GraphUtils.getBaseSettings('Assignment Type Radar',
'How are you doing for different types of assignment'),
// Radar settings
radar:
{
// shape: 'circle',
name:
{
textStyle: GraphUtils.pieTextStyle()
},
splitArea:
{
areaStyle:
{
color:
[
'rgb(255,161,151)',
'rgb(255,190,184)',
'rgba(255,225,199)',
'rgba(255,250,216)',
'rgba(241,255,237)',
],
opacity: 0.4
}
},
indicator: this.course.assignmentTypes.map((t, i) => {return {
name: `${t.name}\n${t.percent}%`,
max: 100,
min: min - 30,
color: Constants.THEME.colors[i]
}}),
radius: '60%',
center: ['50%', '55%']
},
// Data
series:
{
type: 'radar',
data:
[
{
name: 'Score',
symbol: 'circle',
areaStyle:
{
color:
{
type: 'radial',
x: 0.5, y: 0.55, r: 0.5,
colorStops:
[
{offset: 0, color: '#ffa0a0'},
{offset: 0.5, color: '#fffead'},
{offset: 1, color: '#d1ffde'}
],
global: false // 缺省为 false
},
opacity: 0.2
},
value: this.course.assignmentTypes.map(t => t.percent)
}
]
},
color: '#6771c1'
};
return settings;
}
}
@@ -0,0 +1,7 @@
<template>
<div id="type-radar">
<ve-radar height="420px" class="graph" :extend="{a: this.course.name}" :after-config="afterConfig"/>
</div>
</template>
<script src="./type-radar.ts" lang="ts"></script>
+17 -11
View File
@@ -1,14 +1,12 @@
import {Component, Prop, Vue} from 'vue-property-decorator';
import {Course} from '@/components/app/app';
import {GPAUtils} from '@/utils/gpa-utils';
import Course from '@/logic/course';
import {GPAUtils} from '@/logic/utils/gpa-utils';
import Constants from '@/constants';
import {FormatUtils} from '@/utils/format-utils';
import {FormatUtils} from '@/logic/utils/format-utils';
@Component({
})
@Component
export default class OverallBar extends Vue
{
// @ts-ignore
@Prop({required: true}) courses: Course[];
/**
@@ -54,15 +52,16 @@ export default class OverallBar extends Vue
// Data
series:
[
// Max GP
{
type: 'bar',
barGap: '-100%',
data: this.courses.map(course =>
{
return {value: [course.name, GPAUtils.getGP(course, 'A+')],
itemStyle: {color: '#d8d8d8'}}
return {value: [course.name, GPAUtils.getGP(course, 'A+')], itemStyle: {color: '#d8d8d8'}}
}),
},
// Current GP
{
type: 'bar',
barGap: '-100%',
@@ -93,14 +92,21 @@ export default class OverallBar extends Vue
{
let data: any = [];
this.courses.forEach(course =>
this.courses.forEach((course, index) =>
{
// Get GP
let gp = GPAUtils.getGP(course, course.letterGrade);
// No grade cases
if (gp == -1) return;
// Push data
data.push(
{
value: [course.name, GPAUtils.getGP(course, course.letterGrade)],
value: [course.name, gp],
itemStyle:
{
color: Constants.THEME.colors[data.length]
color: Constants.THEME.colors[index]
}
});
});
@@ -1,7 +1,6 @@
<template>
<div id="overall-bar">
<ve-bar height="450px" class="graph"
:extend="chartSettings"></ve-bar>
<ve-bar height="450px" class="graph" :extend="chartSettings"/>
</div>
</template>
@@ -1,6 +1,6 @@
// Row
.unread-entry
.assignment-entry
{
height: 40px;
padding: 0 10px 0 20px;
@@ -11,11 +11,15 @@
// Date
.el-col.date
{
min-width: 130px;
min-width: 150px;
span.month
{
margin-right: 5px;
// Unified width
display: inline-block;
min-width: 50px;
}
span.now
@@ -53,6 +57,16 @@
text-align: right;
float: right;
// Fix smaller screen display issues.
width: unset;
// Status / Problems
span.status
{
margin-right: 8px;
}
// Percentage score
span.percent
{
font-style: italic;
@@ -95,9 +109,40 @@
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;
}
}
.unread-entry:first-child
// 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;
@@ -0,0 +1,74 @@
<template>
<div class="assignment-entry vertical-center"
:class="narrow ? 'narrow' : ''"
:style="`background: ${backgroundColor}`">
<el-row class="unread-row">
<el-col :span="3" class="date">
<span class="month">{{getMoment(assignment.time).format("MMM D")}}</span>
<span class="now">({{getMoment(assignment.time).fromNow()}})</span>
</el-col>
<el-col :span="15" class="description">
<span class="type entry-box"
:style="`border-color: var(--assignment-type-${assignment.typeId})`">
{{assignment.type}}
</span>
<span class="text">{{assignment.description}}</span>
</el-col>
<el-col :span="6" class="grade">
<span v-if="assignment.problem" class="status entry-box" :style="{color: assignment.problemColor}">
{{assignment.problem}}
</span>
<span v-if="assignment.graded" class="percent entry-box">
{{(assignment.score / assignment.scoreMax * 100).toFixed(1)}}
<span class="symbol">%</span>
</span>
<span v-if="assignment.graded" class="score entry-box">{{assignment.score}}</span>
<span v-if="assignment.graded" class="max entry-box">{{assignment.scoreMax}}</span>
<el-button class="mark-as-read" :class="unread ? 'unread' : 'no-unread'"
size="mini" type="text" icon="el-icon-close"
@click="markAsRead">
</el-button>
</el-col>
</el-row>
</div>
</template>
<script lang="ts">
import {Component, Prop, Vue} from 'vue-property-decorator';
import moment from 'moment';
import {Assignment} from '@/logic/course';
@Component
export default class AssignmentEntry extends Vue
{
@Prop({required: true}) assignment: Assignment;
@Prop({default: false}) unread: boolean;
@Prop({default: '#f5f7fa'}) backgroundColor: string;
@Prop({default: false}) narrow: boolean;
/**
* Format a date to the displayed format
*
* @param date Date
*/
getMoment(date: number)
{
return moment(new Date(date));
}
/**
* Mark this unread assignment as read
*/
markAsRead()
{
// Call custom event
this.$emit('mark-as-read', this.assignment)
}
}
</script>
<style src="./assignment-entry.scss" lang="scss" scoped/>
@@ -0,0 +1,116 @@
// Main card content
.course-card-content.main
{
// Main color
background: white;
// Alignment
display: block;
padding: 20px;
height: 50px;
}
#block-info
{
// Align left
text-align: left;
float: left;
#name
{
overflow: hidden;
font-size: 22px;
color: var(--main);
}
#teacher
{
font-size: 12px;
color: #999999;
font-style: italic;
}
}
#block-grade
{
// Align right
text-align: right;
float: right;
// Adjust position
margin-top: -2px;
margin-left: 10px;
#grade
{
font-size: 21px;
}
#updates
{
font-size: 14px;
#unread-number
{
display: inline-block;
width: 20px;
text-align: center;
border-radius: 5px;
padding-left: 3px;
padding-right: 3px;
margin-right: 3px;
}
#unread-text
{
font-style: italic;
}
}
#updates.unread
{
#unread-number
{
background: var(--unread);
color: white;
}
#unread-text
{
color: var(--unread);
}
}
#updates.none
{
color: #999999;
#unread-number
{
background: #eeeeee;
}
}
}
#block-term-grades
{
// Align right
width: auto;
float: right;
margin-right: 10px;
color: gray;
#term, #term-numeric
{
font-size: 11px;
color: #b3b3b3;
}
#term-letter
{
font-size: 14px;
}
}
@@ -1,47 +1,43 @@
<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.toFixed(2)}}</span>
<span class="percent">%</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 id="course-head" class="course-card-content main vertical-center"
:class="clickable ? 'clickable' : ''" @click="redirect">
<div id="block-info">
<div id="name">{{course.name}}</div>
<div id="teacher">{{course.teacherName}}</div>
</div>
<div id="block-grade">
<div id="grade">
<span id="letter">{{course.letterGrade}} </span>
<span id="numeric">{{course.numericGrade === undefined ? '--' : course.numericGrade.toFixed(2)}}</span>
<span id="percent" v-if="course.numericGrade !== undefined">%</span>
</div>
<div id="updates" @click="redirect" :class="unread === 0 ? 'none' : 'unread'">
<span id="unread-number">{{unread}}</span>
<span id="unread-text" :class="clickable ? 'clickable' : ''">new update{{unread >= 2 ? 's' : ''}}</span>
</div>
</div>
<div id="block-term-grades" v-if="course.rawSelectedTerm === -1"
v-for="term in course.allGradingPeriods.slice().reverse()">
<div id="term">Term {{term + 1}}</div>
<div id="term-letter">{{course.letterGradeTerm(term)}}</div>
<div id="term-numeric">{{course.numericGradeTerm(term).toFixed(1)}}%</div>
</div>
</div>
</template>
<script lang="ts">
import {Component, Prop, Vue} from 'vue-property-decorator';
import App, {Course} from '@/components/app/app';
import {CourseUtils} from '@/utils/course-utils';
import Navigation from '@/components/navigation/navigation';
import Course from '@/logic/course';
import App from '@/components/app/app';
@Component({
components: {}
})
@Component
export default class CourseHead extends Vue
{
// @ts-ignore
@Prop() unread: number;
@Prop({required: true}) unread: number;
// @ts-ignore
@Prop() course: Course;
@Prop({required: true}) course: Course;
// @ts-ignore
@Prop() clickable: boolean;
@Prop({required: true}) clickable: boolean;
/**
* Redirect to the course page
@@ -49,102 +45,9 @@
redirect()
{
if (!this.clickable) return;
Navigation.instance.updateIndex(CourseUtils.formatTabIndex(this.course));
App.instance.nav.updateIndex(this.course.urlIndex);
}
}
</script>
<style lang="scss">
// 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;
}
}
}
</style>
<style src="./course-head.scss" lang="scss" scoped/>
@@ -1,53 +0,0 @@
import {Component, Prop, Vue} from 'vue-property-decorator';
import App, {Assignment, Course} from '@/components/app/app';
import {GPAUtils} from '@/utils/gpa-utils';
import Constants from '@/constants';
import UnreadEntry from '@/pages/overall/overall-course/unread-entry/unread-entry';
import CourseHead from '@/pages/overall/overall-course/course-head/course-head.vue';
@Component({
components: {UnreadEntry, CourseHead}
})
export default class OverallCourse extends Vue
{
// @ts-ignore
@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)
}
})
}
}
@@ -1,18 +1,47 @@
<template>
<div id="overall-course">
<el-card class="course-card">
<course-head :clickable="true" :course="course" :unread="countUnread()"></course-head>
<course-head :clickable="true" :course="course" :unread="unread()"/>
<div class="course-card-content expand"
v-if="countUnread() !== 0">
<unread-entry v-for="assignment in unreadAssignments"
v-if="unread() !== 0">
<assignment-entry v-for="assignment in unreadAssignments()"
:assignment="assignment"
:key="assignment.id"
v-on:mark-as-read="markAsRead">
</unread-entry>
unread="true"
v-on:mark-as-read="assignment.markAsRead()">
</assignment-entry>
</div>
</el-card>
</div>
</template>
<script src="./overall-course.ts" lang="ts"></script>
<style src="./overall-course.scss" lang="scss" scoped></style>
<script lang="ts">
import {Component, Prop, Vue} from 'vue-property-decorator';
import AssignmentEntry from '@/pages/overall/overall-course/assignment-entry/assignment-entry.vue';
import CourseHead from '@/pages/overall/overall-course/course-head/course-head.vue';
import Course, {Assignment} from '@/logic/course';
@Component({
components: {AssignmentEntry, CourseHead}
})
export default class OverallCourse extends Vue
{
@Prop({required: true}) course: Course;
mounted()
{
this.unreadAssignments().forEach(a => a.addCallback(() => this.$forceUpdate()));
}
unreadAssignments(): Assignment[]
{
return this.course.assignments.filter(a => a.unread);
}
unread(): number
{
return this.unreadAssignments().length;
}
}
</script>
<style src="./overall-course.scss" lang="scss" scoped/>
@@ -1,30 +0,0 @@
import {Component, Prop, Vue} from 'vue-property-decorator';
import {Assignment, Course} from '@/components/app/app';
import moment from 'moment';
@Component({
})
export default class UnreadEntry extends Vue
{
// @ts-ignore
@Prop({required: true}) assignment: Assignment;
/**
* 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)
}
}
@@ -1,31 +0,0 @@
<template>
<div class="unread-entry vertical-center">
<el-row class="unread-row">
<el-col :span="3" class="date">
<span class="month">{{getMoment(assignment.date).format("MMM Do")}}</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" size="mini" type="text"
icon="el-icon-close" @click="markAsRead">
</el-button>
</el-col>
</el-row>
</div>
</template>
<script src="./unread-entry.ts" lang="ts"></script>
<style src="./unread-entry.scss" lang="scss" scoped></style>
+159 -159
View File
@@ -1,180 +1,180 @@
import {Component, Prop, Vue} from 'vue-property-decorator';
import {Course} from '@/components/app/app';
import moment from 'moment';
import Course from '@/logic/course';
import {CourseUtils} from '@/logic/utils/course-utils';
import GraphUtils from '@/logic/utils/graph-utils';
import {GPAUtils} from '@/logic/utils/gpa-utils';
import Constants from '@/constants';
import Navigation from '@/components/navigation/navigation';
@Component({
})
@Component
export default class OverallLine extends Vue
{
// @ts-ignore
@Prop({required: true}) courses: Course[];
private settings =
{
// Title
title:
{
show: true,
textStyle:
{
fontSize: 12
},
text: 'Average Grade',
subtext: 'Average score trend for every course',
x: 'center'
},
// Legend
legend:
{
show: false,
//left: 'auto',
//align: 'left',
//orient: 'vertical'
textStyle:
{
fontSize: 11
},
icon: 'circle'
},
// Zoom bar
dataZoom:
[
// TODO: Calculate real value for startValue
{
startValue: '9/13/2019'
},
{
type: 'inside'
}
],
series:
{
smooth: true
},
xAxis:
{
//type: 'time'
},
yAxis:
{
min: (value: any) => Math.floor(value.min),
max: (value: any) => value.max
}
};
filteredCourses: Course[];
settings: any;
/**
* Convert assignments list to a graph dataset.
* When this component is created
*/
get convertChart()
created()
{
let courses = this.courses;
// Filter courses
this.filteredCourses = this.courses.filter(c => c.isGraded && 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 => new Date(course.assignments[course.assignments.length - 1].date).getTime());
let minDate: Date = new Date(Math.min.apply(null, minDates));
// Find the dates in between
let now = new Date();
let dates = [];
for (let date = minDate; date <= now; date.setDate(date.getDate() + 1))
// Generate settings
this.settings =
{
dates.push(new Date(date));
}
...GraphUtils.getBaseSettings('Average Grade', 'Average score trend for every course'),
// Compute the rows data
let rows: {[index: string]: any}[] = [];
dates.forEach(date =>
{
// Define row object
let row: {[index: string]:any} = {'date': date.toLocaleDateString('en-US')};
// Zoom bar
dataZoom:
[
{
type: 'slider',
startValue: this.getStartDate(),
// Loop through courses
courses.forEach(course =>
// Minimum zoom: 1 week
minValueSpan: 7 * 24 * 60 * 60 * 1000
}
],
// Tooltip
tooltip:
{
// Total Mean
if (course.grading.method == 'TOTAL_MEAN')
... GraphUtils.tooltipCssShadow(),
trigger: 'axis'
},
// Axis
xAxis:
{
type: 'time',
axisLabel:
{
let score = 0;
let max = 0;
// Loop through assignments
course.assignments.forEach(assignment =>
{
// If assignment should be displayed
if (assignment.complete != 'Complete') return;
// Date is being looked at
let assignmentDate = new Date(assignment.date);
if (assignmentDate.getTime() < date.getTime())
{
// Record scores
score += assignment.score;
max += assignment.scoreMax;
}
});
// Add average to the row
row[course.name] = score / max * 100;
}
else if (course.grading.method == 'PERCENT_TYPE')
formatter: (name: any) => moment(name).format('MMM DD')
},
},
yAxis:
{
axisLabel:
{
let typeScores: {[index: string]: any} = {};
let typeCounts: {[index: string]: any} = {};
formatter: (name: any) => name + '%'
},
min: (value: any) => Math.floor(value.min),
max: (value: any) => Math.min(Math.ceil(value.max), 110)
},
// Loop through assignments
course.assignments.forEach(assignment =>
{
// If assignment should be displayed
if (assignment.complete != 'Complete') return;
// Date is being looked at
let assignmentDate = new Date(assignment.date);
if (assignmentDate.getTime() < date.getTime())
{
// Record scores
if (typeScores[assignment.type] == undefined) typeScores[assignment.type] = 0;
typeScores[assignment.type] += assignment.score / assignment.scoreMax;
if (typeCounts[assignment.type] == undefined) typeCounts[assignment.type] = 0;
typeCounts[assignment.type] ++;
}
});
// Count total percentage (This is to avoid less than expected cases)
// Eg. If HW = 25% and Quiz = 75%, I have 1 hw and 0 quiz
// Without total percentage, the avg grade I get is 25%.
let totalPercentage = 0;
for (let type in course.grading.weightingMap)
{
if (typeScores[type] != undefined)
{
totalPercentage += course.grading.weightingMap[type];
}
}
// Count
let score = 0;
for (let type in typeScores)
{
let typeFactor = course.grading.weightingMap[type] / totalPercentage;
score += typeScores[type] * typeFactor / typeCounts[type];
}
// Add average to the row
if (score != 0) row[course.name] = score * 100;
}
});
// Add it to the array
rows.push(row);
});
return {
columns: columns,
rows: rows
// Series data
series: this.series()
}
}
/**
* Override options
*
* @param options Original options (Unused)
*/
afterConfig(options: any)
{
return this.settings;
}
/**
* Get starting date
*/
private getStartDate()
{
// If it's a past term, use the term's end date, else use today.
let selected = Navigation.instance.getSelectedTerm();
let end = selected == Constants.CURRENT_TERM || selected == -1
? moment() : moment(CourseUtils.getTermEndDate());
return Math.max(end.subtract(30, 'days').toDate().getTime(),
CourseUtils.getTermBeginDate().getTime())
}
/**
* Generate series data
*/
private series()
{
// Each course
let series: any[] = this.filteredCourses.map(course => this.getCourseSeries(course));
// Push other stuff
series.push(
{
type: 'line',
markLine: GraphUtils.getTermLines(),
markArea: GraphUtils.getGradeMarkAreas(0.4)
});
return series
}
/**
* Generate series data for a course
*
* @param course
*/
private getCourseSeries(course: Course)
{
// Graded assignments
let assignments = course.assignments.slice().reverse();
// Create series
return {
name: course.name,
type: 'line',
smooth: true,
symbol: 'circle', // circle, diamond, emptyCircle, none
data: this.toDateRange([...new Set(assignments.map(a => a.time))].map(time =>
{
// Find subset before this assignment
let subset = course.getAssignmentsBefore(time);
// Find grade
if (course.termGrading[subset.term].method == 'PERCENT_TYPE')
return [time, GPAUtils.getPercentTypeAverage(course.termGrading[subset.term], subset.assignments)];
else return [time, GPAUtils.getTotalMeanAverage(subset.assignments)];
}))
}
}
/**
* Convert point data to date range data.
* Eg. [[Mon, 10], [Wed, 5]] to [[Mon, 10], [Tue, 10], [Wed, 5]]
*
* @param data
*/
private toDateRange(data: any[])
{
// Find the min date
let minDate: Date = new Date(data[0][0]);
// Find the dates in between
let now = new Date(Math.min(new Date().getTime(), CourseUtils.getTermEndDate().getTime()));
let times: number[] = [];
for (let date = minDate; date <= now; date.setDate(date.getDate() + 1)) times.push(date.getTime());
// Map the points
let lastValue: any = null;
return times.map(time =>
{
// Data point on this specific date
let thisValue = data.find(a => a[0] == time);
// Switching terms
if (Constants.TERMS.find(t => t.getTime() == time))
lastValue = null;
// Find value
return thisValue == null
? lastValue == null ? null : [time, lastValue[1]]
: [time, (lastValue = thisValue)[1]];
});
}
}
@@ -1,6 +1,6 @@
<template>
<div id="overall-line">
<ve-line :data="convertChart" :extend="settings"></ve-line>
<ve-line :extend="{a: this.courses}" :after-config="afterConfig"/>
</div>
</template>
+18
View File
@@ -34,9 +34,27 @@
font-size: 11px;
}
.no-grade
{
font-size: 30px;
color: #b1b1b1;
// Disable selecting
display:block;
pointer-events: none;
user-select: none;
}
// Cards
.el-card.overall-bar-card
{
margin-right: 20px;
min-width: 170px;
}
.dialog-checkbox
{
display: block;
margin-top: 20px;
margin-bottom: -20px;
}
-24
View File
@@ -1,24 +0,0 @@
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 '@/components/app/app';
import {GPAUtils} from '@/utils/gpa-utils';
@Component({
components: {OverallLine, OverallBar, OverallCourse}
})
export default class Overall extends Vue
{
// @ts-ignore
@Prop({required: true}) courses: Course[];
/**
* This function is called to get gpa since I can't import another
* class in the Vue file.
*/
public getGPA()
{
return GPAUtils.getGPA(this.courses);
}
}
+119 -11
View File
@@ -1,8 +1,22 @@
<template>
<div id="overall">
<el-row>
<el-col :span="4">
<el-card class="large gpa-card vertical-center">
<el-progress v-if="started" :text-inside="true" :percentage="progress()"
:stroke-width="20" status="success" style="margin: 0 20px"/>
<el-dialog title="Notice" :visible.sync="clearUnreadPrompt" @close="clearUnread(false)"
width="30%" style="word-break: unset;">
<span>You have too many new grade notifications. Clear them now?</span>
<img src="./too-many-unread.png" alt=""/>
<el-checkbox class="dialog-checkbox" v-model="dontAskAgain">Don't Ask Again</el-checkbox>
<span slot="footer" class="dialog-footer">
<el-button @click="clearUnread(false)" style="float: left">Nope</el-button>
<el-button type="primary" @click="clearUnread(true)">Sure!</el-button>
</span>
</el-dialog>
<el-row v-if="getGPA().gpa !== -1">
<el-col :span="4" class="overall-span">
<el-card class="large gpa-card vertical-center" body-style="padding: 0">
<div style="padding: 14px;">
<span class="gpa header">GPA:</span>
<span class="gpa text">{{getGPA().gpa}}</span>
@@ -13,18 +27,24 @@
</div>
</el-card>
</el-col>
<el-col :span="14">
<el-card class="large overall-line-card vertical-center">
<overall-line :courses="courses"></overall-line>
<el-col :span="14" class="overall-span">
<el-card class="large overall-line-card vertical-center" body-style="padding: 0 10px">
<overall-line :courses="courses"/>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="large overall-bar-card vertical-center">
<overall-bar :courses="courses"></overall-bar>
<el-col :span="6" class="overall-span">
<el-card class="large overall-bar-card vertical-center" body-style="padding: 0 10px">
<overall-bar :courses="courses"/>
</el-card>
</el-col>
</el-row>
<el-row v-if="getGPA().gpa === -1">
<el-card class="large gpa-card vertical-center">
<div class="no-grade">This quarter has no grades yet...</div>
</el-card>
</el-row>
<overall-course v-for="course in courses"
:course="course"
:key="course.id">
@@ -32,5 +52,93 @@
</div>
</template>
<script src="./overall.ts" lang="ts"></script>
<style src="./overall.scss" lang="scss" scoped></style>
<script lang="ts">
import {Component, Prop, Vue} from 'vue-property-decorator';
import OverallLine from '@/pages/overall/overall-line/overall-line';
import OverallBar from '@/pages/overall/overall-bar/overall-bar';
import OverallCourse from '@/pages/overall/overall-course/overall-course.vue';
import Course, {Assignment} from '@/logic/course';
import {GPAUtils} from '@/logic/utils/gpa-utils';
@Component({
components: {OverallLine, OverallBar, OverallCourse}
})
export default class Overall extends Vue
{
@Prop({required: true}) courses: Course[];
/**
* This function is called to get gpa since I can't import another
* class in the Vue file.
*/
getGPA()
{
return GPAUtils.getGPA(this.courses);
}
// For clear unread prompt
unread: Assignment[];
clearUnreadPrompt = false;
dontAskAgain = false;
started = false;
/**
* Mark as read progress
*/
progress()
{
return +(this.unread.filter(a => !a.unread).length / this.unread.length * 100).toFixed(1);
}
/**
* On page load - check if the user has too many notifications
*/
mounted()
{
// Check unread
if (!this.$cookies.isKey('va.ignore-unread'))
{
// Count unread
this.unread = this.courses.flatMap(c => c.assignments.filter(a => a.unread));
// Prompt clear
if (this.unread.length > 15)
{
this.clearUnreadPrompt = true;
}
}
}
/**
* Clear unread
*
* @param confirmed
*/
clearUnread(confirmed: boolean)
{
// Hide prompt
this.clearUnreadPrompt = false;
// Not confirmed, do nothing
if (!confirmed)
{
if (!this.dontAskAgain) return;
// Don't ask again
this.$cookies.set('va.ignore-unread', true);
return;
}
// Clear unread
this.started = true;
this.unread.forEach((a, i) =>
{
// Delay: 100ms per assignment
// I don't want my server to explode lol
setTimeout(() => a.markAsRead().then(() => this.$forceUpdate()), 100 * i);
});
}
}
</script>
<style src="./overall.scss" lang="scss" scoped/>
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

+1 -1
View File
@@ -1,4 +1,4 @@
import Vue, { VNode } from 'vue';
import Vue, {VNode} from 'vue';
declare global {
namespace JSX {
+4 -3
View File
@@ -1,4 +1,5 @@
declare module '*.vue' {
import Vue from 'vue';
export default Vue;
declare module '*.vue'
{
import Vue from 'vue';
export default Vue;
}
+45
View File
@@ -0,0 +1,45 @@
<template>
<div id="info">
<div id="top">
<div id="title">Veracross Analyzer for SJP</div>
<div id="subtitle">Know your grades better.</div>
</div>
</div>
</template>
<script lang="ts">
import {Component, Vue} from 'vue-property-decorator';
@Component
export default class Info extends Vue
{
}
</script>
<style lang="scss" scoped>
#top
{
padding: 40vh 0;
// Center and scale the image nicely
background-position: center;
background-repeat: no-repeat;
background-size: cover;
text-align: right;
#title
{
font-size: 30px;
margin-right: 10vw;
color: white;
}
#subtitle
{
margin-right: 10vw;
color: white;
}
}
</style>
-48
View File
@@ -1,48 +0,0 @@
import {Course} from '@/components/app/app';
export class CourseUtils
{
/**
* Return a list of courses that are graphed
*
* @param original Original course list
* @return Course[] Filtered course list
*/
public static getGradedCourses(original: Course[]): Course[]
{
// Define result
let result: Course[] = [];
// Filter through courses
original.forEach(course =>
{
// Skip future or past courses
if (course.status != 'active') return;
// Skip courses without levels
if (course.level == 'None') return;
// Skip courses without graded assignments
if (course.assignments.filter(a => a.complete == 'Complete').length == 0) return;
// Skip if there are no grading scale
// if (course.grading.method == 'NOT_GRADED') return;
// Add it to the list
result.push(course);
});
return result;
}
/**
* 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('-')}`;
}
}
-130
View File
@@ -1,130 +0,0 @@
/**
* This is an utility class to calculate GPA.
*/
import {Course} from '@/components/app/app';
export class GPAUtils
{
// [[Min score, Letter grade, Base GPA], ...]
public static SCALE =
[
[96.5, 'A+', 4.00],
[92.5, 'A' , 3.75],
[89.5, 'A-', 3.50],
[86.5, 'B+', 3.25],
[82.5, 'B' , 3.00],
[79.5, 'B-', 2.75],
[76.5, 'C+', 2.50],
[72.5, 'C' , 2.25],
[70.5, 'C-', 2.00],
[69.5, 'D' , 1.00],
[0 , 'F' , 0.00]
];
// Keywords
public static MIN = 0;
public static LETTER = 1;
public static GPA = 2;
/**
* Calculate GPA for a list of couses
*
* @param coursesOriginal List of courses
*/
public static getGPA(coursesOriginal: Course[]): {gpa: number, accurate: boolean, max: number}
{
// Clone array
let courses: Course[] = [];
// Accurate or not
let accurate: boolean = true;
// Remove all courses that does not have a grade
coursesOriginal.forEach(course =>
{
if (course.letterGrade == null || course.letterGrade == '')
{
accurate = false;
}
else if (course.level != 'none')
{
courses.push(course);
}
});
// If no course have grade, return -1
if (courses.length == 0)
{
return {gpa: -1, accurate: false, max: -1};
}
// Count total GPA
let totalGPA = 0;
let maxTotal = 0;
courses.forEach(course =>
{
totalGPA += this.getGP(course, course.letterGrade);
maxTotal += this.getGP(course, 'A+');
});
// Get average GPA, round to two decimal places
let gpa = Math.round(totalGPA / courses.length * 100) / 100;
let maxGPA = Math.round(maxTotal / courses.length * 100) / 100;
// Return results
return {gpa: gpa, accurate: accurate, max: maxGPA};
}
/**
* Calculate GPA for a course
*
* @param course Course
* @param letterGrade Letter grade
*/
public static getGP(course: Course, letterGrade?: string): number
{
// Find the GPA for this course.
for (let scale of this.SCALE)
{
// Letter grades are the same
if (scale[this.LETTER] == letterGrade)
{
// Get grade and add it
let grade = <number> scale[this.GPA];
// Add scaleUp if not failed.
if (grade != 0) grade += course.scaleUp;
// That's it
return grade;
}
}
return -1;
}
/**
* Calculate the total-mean (total/max) average
*
* @param course Course
*/
public static getTotalMeanAverage(course: Course)
{
let score = 0;
let max = 0;
// Loop through assignments
course.assignments.forEach(assignment =>
{
// If assignment should be displayed
if (assignment.complete != 'Complete') return;
// Record scores
score += assignment.score;
max += assignment.scoreMax;
});
// Return
return score / max * 100;
}
}
-38
View File
@@ -1,38 +0,0 @@
import {Assignment} from '@/components/app/app';
export default class JsonUtils
{
/**
* This method filters the information provided in an assignments json.
*
* @param assignments Assignments object
* @returns Assignment[] Filtered assignment grade object list
*/
public static filterAssignments(assignments: any): Assignment[]
{
let result: Assignment[] = [];
assignments.assignments.forEach((assignment: any) =>
{
result.push(
{
id: assignment.assignment_id,
scoreId: assignment.score_id,
type: assignment.assignment_type,
typeId: assignment.assignment_type_id,
description: assignment.assignment_description,
date: assignment._date,
complete: assignment.completion_status,
include: assignment.include_in_calculated_grade == 1,
display: assignment.display_grade == 1,
unread: assignment.is_unread == 1,
scoreMax: assignment.maximum_score,
score: +assignment.raw_score
});
});
return result;
}
}
+5 -4
View File
@@ -15,16 +15,17 @@
"webpack-env"
],
"paths": {
"@/*": [
"src/*"
]
"@/*": ["src/*"]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
],
// Custom
"strictPropertyInitialization": false
},
"include": [
"src/**/*.ts",
+3 -1
View File
@@ -2,7 +2,9 @@
"defaultSeverity": "warning",
"linterOptions": {
"exclude": [
"node_modules/**"
"node_modules/**",
"*.json",
"**/*.json"
]
},
"rules": {