Compare commits

..

287 Commits

Author SHA1 Message Date
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
52 changed files with 4567 additions and 3167 deletions
+3
View File
@@ -1,5 +1,8 @@
#!/usr/bin/env bash #!/usr/bin/env bash
#echo "Switch to production database settings"
#exit
# abort on errors # abort on errors
set -e set -e
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 51 KiB

+3375 -2611
View File
File diff suppressed because it is too large Load Diff
+12 -10
View File
@@ -9,25 +9,27 @@
}, },
"dependencies": { "dependencies": {
"@types/chroma-js": "^1.4.3", "@types/chroma-js": "^1.4.3",
"@types/md5": "^2.1.33",
"chroma-js": "^2.1.0", "chroma-js": "^2.1.0",
"core-js": "^2.6.5", "core-js": "^2.6.10",
"echarts": "^4.2.1", "echarts": "^4.5.0",
"element-ui": "^2.11.1", "element-ui": "^2.13.0",
"md5": "^2.2.1",
"moment": "^2.24.0", "moment": "^2.24.0",
"p-wait-for": "^3.1.0", "p-wait-for": "^3.1.0",
"v-charts": "^1.19.0", "v-charts": "^1.19.0",
"vue": "^2.6.10", "vue": "^2.6.10",
"vue-class-component": "^7.0.2", "vue-class-component": "^7.0.2",
"vue-cookies": "^1.5.13", "vue-cookies": "^1.5.13",
"vue-property-decorator": "^8.1.0" "vue-property-decorator": "^8.3.0"
}, },
"devDependencies": { "devDependencies": {
"@vue/cli-plugin-babel": "^3.10.0", "@vue/cli-plugin-babel": "^3.12.1",
"@vue/cli-plugin-typescript": "^3.10.0", "@vue/cli-plugin-typescript": "^3.12.1",
"@vue/cli-service": "^3.10.0", "@vue/cli-service": "^4.1.1",
"node-sass": "^4.9.0", "node-sass": "^4.13.0",
"sass-loader": "^7.1.0", "sass-loader": "^7.3.1",
"typescript": "^3.4.3", "typescript": "^3.7.3",
"vue-template-compiler": "^2.6.10" "vue-template-compiler": "^2.6.10"
}, },
"postcss": { "postcss": {
+3 -1
View File
@@ -24,7 +24,9 @@
<!-- ElementUI --> <!-- ElementUI -->
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css"> <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<!-- Fonts -->
<link href="https://fonts.googleapis.com/css?family=Nunito+Sans&display=swap" rel="stylesheet">
</body> </body>
<!-- Global site tag (gtag.js) - Google Analytics --> <!-- 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.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 44 KiB

+20 -11
View File
@@ -25,7 +25,10 @@
--assignment-type-3: #ff9900; --assignment-type-3: #ff9900;
--assignment-type-4: #b02b02; --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
@@ -54,16 +57,17 @@
} }
// Overall // Overall
#overall, #overall-course, .overall-span, #app-content #app-inner, #overall, #overall-course, .overall-span, #app-content
{ {
background: var(--dark-layer-1) !important; background: var(--dark-layer-1) !important;
} }
// Course card // Course card
.entry-box, .none .unread-number {background: #a1a1a1 !important} .entry-box, .none .unread-number {background: #797979 !important}
.entry-box.max {background-color: #949494 !important} .entry-box.max {background-color: #949494 !important}
.entry-box.percent {background-color: #a7a490 !important} .entry-box.percent {background-color: #a7a490 !important}
.course-name {color: #cffff6 !important} .course-name {color: #cffff6 !important}
#block-grade #updates.none #unread-number {background: #757575 !important}
.course-card-content.expand, .assignment-entry, .unread-row, .course-card-content.expand, .assignment-entry, .unread-row,
.unread-row .el-col, #assignment-type-head, .course-page-graph.el-col .unread-row .el-col, #assignment-type-head, .course-page-graph.el-col
@@ -71,14 +75,8 @@
background-color: var(--dark-layer-3) !important; background-color: var(--dark-layer-3) !important;
} }
.overall-span.el-col, .course-page-graph.el-col // Nav bar
{ .el-menu--horizontal>.el-menu-item.is-active {color: var(--dark-foreground) !important;}
div, span
{
background: #f9f9f9 !important;
color: var(--dark-layer-1) !important;
}
}
} }
// ############## // ##############
@@ -136,3 +134,14 @@ div.el-card.course-card > div.el-card__body
-ms-user-select: none; -ms-user-select: none;
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;
}
+72 -50
View File
@@ -5,65 +5,79 @@ import Overall from '@/pages/overall/overall.vue';
import Constants from '@/constants'; import Constants from '@/constants';
import pWaitFor from 'p-wait-for'; import pWaitFor from 'p-wait-for';
import {HttpUtils} from '@/logic/utils/http-utils'; import {HttpUtils} from '@/logic/utils/http-utils';
import {GPAUtils} from '@/logic/utils/gpa-utils'; import Loading from '@/components/overlays/loading.vue';
import Loading from '@/components/loading/loading.vue';
import CoursePage from '@/pages/course/course-page.vue'; import CoursePage from '@/pages/course/course-page.vue';
import Course from '@/logic/course'; import Course from '@/logic/course';
import LoginUser from '@/logic/login-user';
import NavController from '@/logic/nav-controller';
import Info from '@/statics/Info.vue';
@Component({ @Component({
components: {Login, Navigation, Overall, Loading, CoursePage}, components: {Login, Navigation, Overall, Loading, CoursePage, Info},
}) })
export default class App extends Vue export default class App extends Vue
{ {
// Is the login panel shown // Is the login panel shown
public showLogin: boolean = true; showLogin: boolean = true;
// List of course that the student takes // List of course that the student takes
public courses: Course[] = []; courses: Course[] = [];
gradedCourses: Course[] = [];
// List of course that should be displayed
public filteredCourses: Course[] = [];
// Currently selected tab
public selectedTab: string = 'overall';
// Are the course assignments loaded from the server. // Are the course assignments loaded from the server.
public assignmentsReady: boolean = false; assignmentsReady: boolean = false;
// Token // Token
public token: string = ''; user: LoginUser = null as any;
// Loading text // Loading text
public loading: string = ''; loading: string = '';
// Loading error // Loading error
public loadingError: boolean = false; loadingError: boolean = false;
// Navigation controller
nav: NavController = new NavController();
// Http Client // Http Client
public static http: HttpUtils = new HttpUtils(); static http: HttpUtils = new HttpUtils();
// Instance // 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. * This is called when the instance is created.
*/ */
public created() created()
{ {
// Show splash // Show splash
console.log(Constants.SPLASH); console.log(Constants.SPLASH);
// Update instance // Update instance
App.instance = this; 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. * 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 // Hide login bar
this.showLogin = false; this.showLogin = false;
@@ -71,20 +85,20 @@ export default class App extends Vue
// Show loading message // Show loading message
this.logLoading('1. Logging in...'); this.logLoading('1. Logging in...');
// Store token // Store user
this.token = token; this.user = user;
// Assign token to http client // Assign user to http client
App.http.token = token; App.http.user = user;
// Load data // Load data
this.loadCoursesAfterLogin(); this.loadCoursesAfterLogin();
} }
/** /**
* Load courses data after login. * Load courses data after logging in.
*/ */
public loadCoursesAfterLogin() loadCoursesAfterLogin()
{ {
// Show loading message // Show loading message
this.logLoading('2. Loading courses...'); this.logLoading('2. Loading courses...');
@@ -109,7 +123,7 @@ export default class App extends Vue
/** /**
* Load the assignments of the courses * Load the assignments of the courses
*/ */
public loadAssignments() loadAssignments()
{ {
// Show loading message // Show loading message
this.logLoading('3. Loading assignments...'); this.logLoading('3. Loading assignments...');
@@ -134,7 +148,7 @@ export default class App extends Vue
pWaitFor(() => this.courses.every(c => c.rawAssignments != null)).then(() => pWaitFor(() => this.courses.every(c => c.rawAssignments != null)).then(() =>
{ {
// Filter courses // Filter courses
this.filteredCourses = this.courses.filter(c => c.isGraded); this.gradedCourses = this.courses.filter(c => c.isGraded);
// Check grading algorithms // Check grading algorithms
this.checkGradingAlgorithms(); this.checkGradingAlgorithms();
@@ -144,42 +158,50 @@ export default class App extends Vue
/** /**
* Check the courses' grading algorithms. (Total-mean or percent-type) * Check the courses' grading algorithms. (Total-mean or percent-type)
*/ */
private checkGradingAlgorithms() checkGradingAlgorithms()
{ {
// Show loading message // Show loading message
this.logLoading('4. Checking grading algorithms...'); this.logLoading('4. Checking grading algorithms...');
// Loop through all the courses // Loop through all the courses
for (const course of this.filteredCourses) for (const course of this.gradedCourses)
{ {
let termGrade = +GPAUtils.getTotalMeanAverage(course.computed.termAssignments[Constants.CURRENT_TERM]).toFixed(2); for (const i of [0, 1, 2, 3])
{
const cookieIndex = `va.grading.${i}.${course.assignmentsId}`;
// Check if total-average grade is the same with percent-type grade // Check if already exist in cookies
if (course.rawNumericGrade == termGrade) if (this.$cookies.isKey(cookieIndex))
{ {
course.grading = {method: 'TOTAL_MEAN', weightingMap: {}}; course.termGrading[i] = {method: 'TOTAL_MEAN', weightingMap: {}};
} continue;
else }
{
// Request grading scheme for this course // Request grading scheme for this course at this grading period
App.http.post('/grading', {'assignmentsId': course.assignmentsId}).then(response => App.http.post('/grading/term', {assignmentsId: course.assignmentsId, term: i}).then(resp =>
{ {
// Check success // Check success
if (response.success) if (resp.success)
{ {
// Add it to course // 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})`)) .catch(e => this.showError(`Error: Grading data failed to load.\n(${e})`))
} }
} }
// Wait for done // 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; this.assignmentsReady = true;
// Remove loading // Remove loading
@@ -192,7 +214,7 @@ export default class App extends Vue
* *
* @param message Message * @param message Message
*/ */
private logLoading(message: string) logLoading(message: string)
{ {
if (message == '') this.loading = ''; if (message == '') this.loading = '';
else this.loading += '\n' + message; else this.loading += '\n' + message;
@@ -203,7 +225,7 @@ export default class App extends Vue
* *
* @param message Error message * @param message Error message
*/ */
private showError(message: string) showError(message: string)
{ {
this.loadingError = true; this.loadingError = true;
this.loading = message; this.loading = message;
@@ -212,7 +234,7 @@ export default class App extends Vue
/** /**
* Sign out * Sign out
*/ */
public signOut() signOut()
{ {
// Clear all cookies // Clear all cookies
this.$cookies.keys().forEach(key => this.$cookies.remove(key)); this.$cookies.keys().forEach(key => this.$cookies.remove(key));
@@ -226,7 +248,7 @@ export default class App extends Vue
* *
* @param code * @param code
*/ */
public selectTime(code: number) selectTime(code: number)
{ {
// TODO: Optimize // TODO: Optimize
window.location.reload(); window.location.reload();
+20 -14
View File
@@ -1,23 +1,29 @@
<template> <template>
<div id="app" class="theme-default"> <div id="app" class="theme-default">
<login v-if="showLogin" v-on:login:token="onLogin"></login> <div id="app-inner" v-if="staticPage === ''" :class="{dark: darkMode}">
<navigation :courses="filteredCourses" <login v-if="showLogin" v-on:login:user="onLogin"/>
:activeIndex.sync="selectedTab" <navigation v-if="user != null"
@sign-out="signOut" @select-time="selectTime"> :courses="gradedCourses"
</navigation> :user="user"
:nav="nav"
@sign-out="signOut" @select-time="selectTime">
</navigation>
<div id="app-content" v-if="assignmentsReady && loading === ''"> <div id="app-content" v-if="assignmentsReady && loading === ''">
<overall v-if="selectedTab === 'overall'" <overall v-if="nav.id === 'overall'"
:courses="filteredCourses"> :courses="gradedCourses">
</overall> </overall>
<course-page v-if="selectedTab.split('/')[0] === 'course'" <course-page v-if="nav.id === 'course'"
:course="filteredCourses.find(c => +c.id === +selectedTab.split('/')[1])"> :course="gradedCourses.find(c => +c.id === +nav.info.id)">
</course-page> </course-page>
</div>
<loading v-if="loading !== ''" :text="loading" :error="loadingError"/>
</div> </div>
<loading v-if="loading !== ''" :text="loading" :error="loadingError"></loading> <Info v-if="staticPage === 'info'"/>
</div> </div>
</template> </template>
<script src="./app.ts" lang="ts"></script> <script src="./app.ts" lang="ts"></script>
<style src="./app.scss" lang="scss"></style> <style src="./app.scss" lang="scss"/>
+65 -39
View File
@@ -2,48 +2,44 @@ import {Component, Vue} from 'vue-property-decorator';
import Constants from '@/constants'; import Constants from '@/constants';
import App from '@/components/app/app'; import App from '@/components/app/app';
import VersionUtils from '@/logic/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. * This component handles user login, and obtains data from the server.
*/ */
@Component({ @Component({components: {Maintenance}})
components: {},
})
export default class Login extends Vue export default class Login extends Vue
{ {
public username: string = ''; username = '';
public password: string = ''; password = '';
public loading: boolean = false; loading = false;
public error: string = ''; error = '';
disableInput = false;
maintenance = '';
/** /**
* This is called when the instance is created. * This is called when the instance is created.
*/ */
public created() created()
{ {
// Check login cookies // Check login cookies
if (this.$cookies.isKey('va.token')) if (this.$cookies.isKey('va.token'))
{ {
// Check cookies version // Check cookies version
if (this.needToUpdateCookies()) if (this.needToUpdateCookies()) this.clearCookies();
{
console.log('Version Updated! Clearing cookies...');
// Clear all cookies
this.$cookies.keys().forEach(key => this.$cookies.remove(key));
}
else else
{ {
// Already contains valid token / TODO: Validate // Show loading
// TODO: Update token each access this.disableInput = this.loading = true;
this.$emit('login:token', this.$cookies.get('va.token'));
// Login with token
this.login('/login/token', {token: this.$cookies.get('va.token')});
} }
} }
else
{
console.log('Cookies doesn\'t exist');
}
} }
/** /**
@@ -51,7 +47,7 @@ export default class Login extends Vue
* *
* @returns boolean Need to clear cookies or not * @returns boolean Need to clear cookies or not
*/ */
public needToUpdateCookies(): boolean needToUpdateCookies(): boolean
{ {
// Version doesn't exist // Version doesn't exist
if (!this.$cookies.isKey('va.version')) return true; if (!this.$cookies.isKey('va.version')) return true;
@@ -61,9 +57,9 @@ 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 // Make login button loading
this.loading = true; this.loading = true;
@@ -71,43 +67,73 @@ export default class Login extends Vue
// Format it // Format it
this.username = this.username.toLowerCase().replace(/ /g, '').replace(/@.*/g, ''); 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 // Fetch request
App.http.post('/login', {username: this.username, password: this.password}) App.http.post(url, data)
.then(response => .then(response =>
{ {
// Check success // Check success
if (response.success) if (response.success)
{ {
// Maintenance
if (response.data.maintenance)
{
this.maintenance = response.data.maintenance;
return;
}
// Save token to cookies // Save token to cookies
this.$cookies.set('va.token', response.data, '27d'); this.$cookies.set('va.token', response.data.user.token, '27d');
this.$cookies.set('va.version', Constants.VERSION, '27d'); this.$cookies.set('va.version', Constants.VERSION, '27d');
// Call custom event with token // Call a custom event with the token
this.$emit('login:token', response.data); this.$emit('login:user', new LoginUser(response.data.user));
} }
else else
{ {
// Show error message // Login expired -> clear cookies
this.error = response.data; if (response.data == 'Error: Login expired')
{
this.clearCookies();
}
// Allow the user to retry // Show error message & allow user to retry
this.loading = false; this.error = response.data;
this.disableInput = this.loading = false;
} }
}) })
.catch(err => .catch(err =>
{ {
alert(err); // Show error message & allow user to retry
this.error = err;
// Allow the user to retry this.disableInput = this.loading = false;
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(); this.onLoginClick();
} }
/**
* Clear cookies
*/
clearCookies()
{
this.$cookies.keys().forEach(key => this.$cookies.remove(key));
}
} }
+5 -1
View File
@@ -9,6 +9,7 @@
<el-input v-model="username" <el-input v-model="username"
placeholder="SJP Username (Eg. flast21)" placeholder="SJP Username (Eg. flast21)"
:class="{'input-error': error !== ''}" :class="{'input-error': error !== ''}"
v-if="!disableInput"
@keyup.enter.native="onEnter"> @keyup.enter.native="onEnter">
</el-input> </el-input>
@@ -16,6 +17,7 @@
placeholder="SJP Password" placeholder="SJP Password"
show-password="" show-password=""
:class="{'input-error': error !== ''}" :class="{'input-error': error !== ''}"
v-if="!disableInput"
@keyup.enter.native="onEnter"> @keyup.enter.native="onEnter">
</el-input> </el-input>
@@ -25,8 +27,10 @@
</form> </form>
</div> </div>
</div> </div>
<Maintenance v-if="maintenance" :message="maintenance"/>
</div> </div>
</template> </template>
<script src="./login.ts" lang="ts"></script> <script src="./login.ts" lang="ts"></script>
<style src="./login.scss" lang="scss"></style> <style src="./login.scss" lang="scss"/>
+16 -1
View File
@@ -16,6 +16,13 @@
} }
} }
#nav-avatar
{
position: absolute;
right: 0;
margin: 10px 20px;
}
#sign-out-button #sign-out-button
{ {
// Float right // Float right
@@ -31,7 +38,7 @@
{ {
// Float right // Float right
position: absolute; position: absolute;
right: 110px; right: 80px;
// Margins // Margins
margin-top: 12px; margin-top: 12px;
@@ -83,6 +90,14 @@
-webkit-text-fill-color: transparent !important; -webkit-text-fill-color: transparent !important;
-webkit-background-clip: text !important; -webkit-background-clip: text !important;
} }
#nav-logo-version
{
color: #a7a7a7;
margin-left: 5px;
margin-top: 8px;
font-size: 10px;
}
} }
#next-course #next-course
+58 -91
View File
@@ -1,31 +1,29 @@
import {Component, Prop, Vue} from 'vue-property-decorator'; import {Component, Prop, Vue} from 'vue-property-decorator';
import App from '@/components/app/app';
import {CourseUtils} from '@/logic/utils/course-utils';
import {FormatUtils} from '@/logic/utils/format-utils';
import pWaitFor from 'p-wait-for';
import Course from '@/logic/course'; import Course from '@/logic/course';
import Constants from '@/constants';
import LoginUser from '@/logic/login-user';
import NavController from '@/logic/nav-controller';
import App from '@/components/app/app';
/** /**
* This component is the top navigation bar * This component is the top navigation bar
*/ */
@Component({ @Component
components: {},
})
export default class Navigation extends Vue export default class Navigation extends Vue
{ {
@Prop({required: true}) activeIndex: string; @Prop({required: true}) nav: NavController;
@Prop({required: true}) courses: Course[]; @Prop({required: true}) courses: Course[];
@Prop({required: true}) user: LoginUser;
private gradingPeriod: string = 'All Year'; private gradingPeriod: string = 'All Year';
// Instance // Instance
public static instance: Navigation; static instance: Navigation;
/** /**
* This is called when the instance is created. * This is called when the instance is created.
*/ */
public created() created()
{ {
// Check selected time // Check selected time
if (!this.$cookies.isKey('va.grading-period')) if (!this.$cookies.isKey('va.grading-period'))
@@ -38,37 +36,9 @@ export default class Navigation extends Vue
/** /**
* This is called when the instance is loaded. * This is called when the instance is loaded.
*/ */
public mounted() mounted()
{ {
// Set instance
Navigation.instance = this; Navigation.instance = this;
// Set history state
let url = '/' + window.location.hash;
if (url == '/' || url == '') url = '/#overall';
window.history.replaceState({lastTab: url.substring(1)}, '', url);
// Update initial index after loading is done
pWaitFor(() => this.courses.length > 1 && App.instance.loading != '').then(() =>
{
this.updateIndex(url.substring(2), false);
});
// Create history state listener
window.onpopstate = e =>
{
if (e.state)
{
// Restore previous tab
console.log(`onPopState: Current: ${this.activeIndex}, Previous: ${e.state.lastTab}`);
this.updateIndex(e.state.lastTab, false);
}
};
}
public formatCourseIndex(course: Course)
{
return CourseUtils.formatTabIndex(course);
} }
/** /**
@@ -77,55 +47,19 @@ export default class Navigation extends Vue
* @param index The index selected * @param index The index selected
* @param indexPath The path of the index * @param indexPath The path of the index
*/ */
public onSelect(index: string, indexPath: string) onSelect(index: string, indexPath: string)
{ {
// Update active index // Update active index
this.updateIndex(index); try
}
/**
* 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)
{ {
// Check url // Is json
let url = `/#${newIndex}`; this.nav.updateIndex(JSON.parse(index))
// Push history state
window.history.pushState({lastTab: newIndex}, '', url);
} }
catch (e)
// Update title
document.title = 'Veracross Analyzer - ' + this.getTitle(newIndex);
// Scroll to top
window.scrollTo(0, 0);
}
/**
* Get title for index
*
* @param index Index
*/
public getTitle(index: string)
{
// Course
if (index.startsWith('course'))
{ {
return this.findCourse(index.split('/')[1], 0).name; // Not json
this.nav.updateIndex(index);
} }
// Others
return FormatUtils.toTitleCase(index);
} }
/** /**
@@ -133,10 +67,10 @@ export default class Navigation extends Vue
* *
* @param indexOffset Index offset (Eg. 1 for next) * @param indexOffset Index offset (Eg. 1 for next)
*/ */
public nextCourse(indexOffset: number) nextCourse(indexOffset: number)
{ {
// Set tab to the next index // Set tab to the next index
this.updateIndex(CourseUtils.formatTabIndex(this.findNextCourse(indexOffset))) this.nav.updateIndex(this.findNextCourse(indexOffset).urlIndex)
} }
/** /**
@@ -144,9 +78,9 @@ export default class Navigation extends Vue
* *
* @param indexOffset Index offset (Eg. 1 for next) * @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);
} }
/** /**
@@ -155,7 +89,7 @@ export default class Navigation extends Vue
* @param courseId Course ID * @param courseId Course ID
* @param indexOffset Index offset (Eg. 1 for next) * @param indexOffset Index offset (Eg. 1 for next)
*/ */
public findCourse(courseId: string, indexOffset: number) findCourse(courseId: string, indexOffset: number)
{ {
// Find current course index // Find current course index
let courseIndex = this.courses.findIndex(c => c.id == +courseId); let courseIndex = this.courses.findIndex(c => c.id == +courseId);
@@ -169,21 +103,54 @@ export default class Navigation extends Vue
* *
* @param command Term 1, Term 2, All Year, etc. * @param command Term 1, Term 2, All Year, etc.
*/ */
public selectGradingPeriod(command: string) selectGradingPeriod(command: string)
{ {
this.gradingPeriod = command; this.gradingPeriod = command;
this.$cookies.set('va.grading-period', command, '10y'); this.$cookies.set('va.grading-period', command, '10y');
// Call event // Call event
this.$emit('select-time', this.getSelectedGradingPeriod()); this.$emit('select-time', this.getSelectedTerm());
} }
/** /**
* Get code for selected time * Get code for selected time
*/ */
public getSelectedGradingPeriod(): number getSelectedTerm(): number
{ {
if (this.gradingPeriod == 'All Year') return -1; if (this.gradingPeriod == 'All Year') return -1;
else return +this.gradingPeriod.replace('Term ', '') - 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}
} }
+24 -11
View File
@@ -1,55 +1,68 @@
<template> <template>
<div id="navigation"> <div id="navigation">
<el-menu style="margin-bottom: 10px;" class="centered" mode="horizontal" <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"> <div id="nav-title">
<img id="nav-logo" alt="logo" src="../../assets/logo.png"> <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-text" class="logo-text">Veracross Analyzer</span>
<span id="nav-logo-version">v{{version}}</span>
</div> </div>
<el-menu-item index="overall">Overall</el-menu-item> <el-menu-item index="overall">Overall</el-menu-item>
<el-submenu index="courses"> <el-submenu index="">
<template slot="title">Courses</template> <template slot="title">Courses</template>
<el-menu-item v-for="course in courses" <el-menu-item v-for="course in courses"
:index="formatCourseIndex(course)" :index="JSON.stringify(course.urlIndex)"
:key="course.name">{{course.name}}</el-menu-item> :key="course.id">{{course.name}}</el-menu-item>
</el-submenu> </el-submenu>
<!-- Grading period selection --> <!-- Grading period selection -->
<el-dropdown id="nav-grading-period" @command="selectGradingPeriod"> <el-dropdown id="nav-grading-period" @command="selectGradingPeriod">
<el-button type="primary" size="medium"> <el-button type="primary" size="medium">
{{gradingPeriod}}<i class="el-icon-arrow-down el-icon--right"></i> {{gradingPeriod}}<i class="el-icon-arrow-down el-icon--right"/>
</el-button> </el-button>
<el-dropdown-menu slot="dropdown"> <el-dropdown-menu slot="dropdown">
<el-dropdown-item command="Term 1">Term 1</el-dropdown-item> <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 2">Term 2</el-dropdown-item>
<el-dropdown-item command="Term 3" disabled>Term 3</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> <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-item command="All Year" divided>All Year</el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</el-dropdown> </el-dropdown>
<el-button @click="$emit('sign-out')" id="sign-out-button" type="text">Sign Out</el-button> <!-- User avatar -->
<el-dropdown id="nav-avatar" trigger="click" @command="onAvatarMenu">
<el-avatar :src="user.avatarUrl"/>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item style="text-align: center">{{user.nickname}}</el-dropdown-item>
<el-dropdown-item icon="el-icon-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> </el-menu>
<!-- Previous course / Next course (Only when the page is courses) --> <!-- Previous course / Next course (Only when the page is courses) -->
<div v-if="activeIndex.includes('course') && findNextCourse(-1) != null" <div v-if="nav.id === 'course' && findNextCourse(-1) != null"
@click="nextCourse(-1)" id="prev-course" class="nav-course-operations unselectable"> @click="nextCourse(-1)" id="prev-course" class="nav-course-operations unselectable">
PREVIOUS COURSE PREVIOUS COURSE
</div> </div>
<footer> <footer>
<div v-if="activeIndex.includes('course') && findNextCourse(1) != null" <div v-if="nav.id === 'course' && findNextCourse(1) != null"
@click="nextCourse(1)" id="next-course" class="nav-course-operations unselectable"> @click="nextCourse(1)" id="next-course" class="nav-course-operations unselectable">
NEXT COURSE NEXT COURSE
</div> </div>
</footer> </footer>
<!-- Back to top --> <!-- Back to top -->
<el-backtop style="box-shadow: rgba(0, 0, 0, 0.23) 0 3px 11px 0;"></el-backtop> <el-backtop style="box-shadow: rgba(0, 0, 0, 0.23) 0 3px 11px 0;"/>
</div> </div>
</template> </template>
<script src="./navigation.ts" lang="ts"></script> <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> <template>
<div id="loading"> <div id="loading">
<div id="text" :class="message()"> <div id="text" :class="message">
{{message()}} {{message}}
<div v-if="!error" class="el-loading-spinner"> <div v-if="!error" class="el-loading-spinner">
<svg viewBox="25 25 50 50" class="circular"> <svg viewBox="25 25 50 50" class="circular">
@@ -10,7 +10,7 @@
</div> </div>
<div v-if="error" id="error-details"> <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}} {{line}}
<br> <br>
</span> </span>
@@ -18,7 +18,7 @@
</div> </div>
<div v-if="!error" id="details"> <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}} {{line}}
<br> <br>
</span> </span>
@@ -29,21 +29,19 @@
<script lang="ts"> <script lang="ts">
import {Component, Prop, Vue} from 'vue-property-decorator'; import {Component, Prop, Vue} from 'vue-property-decorator';
@Component({ @Component
components: {}
})
export default class Loading extends Vue export default class Loading extends Vue
{ {
@Prop({required: true}) text: string; @Prop({required: true}) text: string;
@Prop({required: true}) error: boolean; @Prop({required: true}) error: boolean;
getText() get split()
{ {
return this.text.split('\n'); return this.text.split('\n');
} }
message() get message()
{ {
return this.error ? 'Error' : 'Loading'; 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>
+22 -9
View File
@@ -1,20 +1,23 @@
/** /**
* This class stores the static constants. * This class stores the static constants.
*/ */
import {findLastIndex} from '@/logic/utils/general-utils';
export default class Constants export default class Constants
{ {
/** Base url for api access */ /** Base url for api access */
public static API_URL: string = 'https://va.hydev.org/api'; static API_URL: string = 'https://va.hydev.org/api';
// static API_URL: string = 'http://localhost:24021/api';
/** Current version */ /** Current version */
public static VERSION: string = '0.4.6.1087'; static VERSION: string = '0.5.3.1373';
/** Minimum version that still supports the same cookies */ /** The minimum version that still supports the same cookies */
public static MIN_SUPPORTED_VERSION: string = '0.3.4.561'; 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' + '| |. , |---|,---.,---.| , .,---,,---.,---.\n' +
' \\ / >< | || |,---|| | | .-\' |---\'| \n' + ' \\ / >< | || |,---|| | | .-\' |---\'| \n' +
@@ -24,7 +27,7 @@ export default class Constants
` Github: ${Constants.GITHUB}`; ` Github: ${Constants.GITHUB}`;
// Graph Theme // Graph Theme
public static THEME = static THEME =
{ {
// Colors // Colors
colors: colors:
@@ -56,7 +59,7 @@ export default class Constants
}; };
// Terms (TODO: Actually get the terms dynamically // Terms (TODO: Actually get the terms dynamically
public static TERMS = static TERMS =
[ [
new Date('Sep 04 2019'), new Date('Sep 04 2019'),
new Date('Nov 03 2019'), new Date('Nov 03 2019'),
@@ -64,5 +67,15 @@ export default class Constants
new Date('Mar 22 2020'), new Date('Mar 22 2020'),
new Date('Jun 05 2020'), new Date('Jun 05 2020'),
]; ];
public static CURRENT_TERM = 1; 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);
}
} }
+233 -83
View File
@@ -1,30 +1,129 @@
import JsonUtils from '@/logic/utils/json-utils';
import {FormatUtils} from '@/logic/utils/format-utils'; import {FormatUtils} from '@/logic/utils/format-utils';
import {CourseUtils} from '@/logic/utils/course-utils'; import {CourseUtils} from '@/logic/utils/course-utils';
import Navigation from '@/components/navigation/navigation'; import Navigation from '@/components/navigation/navigation';
import {GPAUtils} from '@/logic/utils/gpa-utils'; 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. * Objects of this interface represent assignment grades.
*/ */
export interface Assignment export class Assignment
{ {
id: number id: number;
scoreId: number scoreId: number;
type: string type: string;
typeId: number typeId: number;
description: string description: string;
time: number time: number;
complete: string complete: string;
include: boolean include: boolean;
display: boolean display: boolean;
unread: boolean unread: boolean;
scoreMax: number scoreMax: number;
score: number score: number;
gradingPeriod: 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
return this.include && (this.complete == 'Complete' || this.complete == 'Late' || this.complete == 'NREQ');
}
/**
* What is the problem with this assignment
*
* @return string Empty string if complete, otherwise return problem.
*/
get problem()
{
switch (this.complete)
{
case 'Complete': return '';
case 'Late': return 'Late';
case 'NREQ': return 'Dropped';
}
}
/**
* Get the text color of the problem
*/
get problemColor()
{
switch (this.complete)
{
case 'Late': return '#ff0036';
case 'NREQ': return '#41b141';
}
}
/**
* 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 export interface AssignmentType
@@ -39,6 +138,12 @@ export interface AssignmentType
assignmentCount: number assignmentCount: number
} }
export interface Grading
{
method: string
weightingMap: {[index: string]: number}
}
export default class Course export default class Course
{ {
id: number; id: number;
@@ -54,17 +159,10 @@ export default class Course
level: string; level: string;
scaleUp: number; scaleUp: number;
grading: termGrading: Grading[];
{ termAssignments: Assignment[][];
method: string
weightingMap: {[index: string]: number}
};
computed: cache: CacheUtils = new CacheUtils();
{
termAssignments: Assignment[][]
allYearGrade: number
};
/** /**
* Construct a course with a course json object * Construct a course with a course json object
@@ -98,7 +196,7 @@ export default class Course
} }
else this.level = 'Unknown'; else this.level = 'Unknown';
this.grading = courseJson.grading; this.termGrading = new Array(4).fill(null);
} }
/** /**
@@ -110,20 +208,16 @@ export default class Course
{ {
// Load assignments // Load assignments
// Parse json and filter it // Parse json and filter it
this.rawAssignments = JsonUtils.filterAssignments(data); this.rawAssignments = data.assignments.map((a: any) => new Assignment(a));
// Sort by date (Latest is at 0) // Sort by date (Latest is at 0)
this.rawAssignments.sort((a, b) => b.time - a.time); this.rawAssignments.sort((a, b) => b.time - a.time);
// Filter assignments into terms // Filter assignments into terms
let termAssignments: Assignment[][] = [[], [], [], []]; this.termAssignments = [[], [], [], []];
let currentTerm = 0;
// Loop through it by time order // Loop through it by time order
this.rawAssignments.forEach(a => termAssignments[a.gradingPeriod].push(a)); this.rawAssignments.forEach(a => this.termAssignments[a.gradingPeriod].push(a));
// Set computed data
this.computed = {termAssignments: termAssignments, allYearGrade: -1};
} }
/** /**
@@ -148,90 +242,124 @@ export default class Course
} }
/** /**
* Get assignments of the selected time * Get currently selected grading periods
*/ */
get rawSelectedAssignments(): Assignment[] get gradingPeriods(): number[]
{ {
let timeCode = Navigation.instance.getSelectedGradingPeriod(); return this.cache.get('GradingPeriods', () =>
// All year
if (timeCode == -1)
{ {
return this.rawAssignments; return (this.rawSelectedTerm == -1 ? [0, 1, 2, 3] : [this.rawSelectedTerm]).filter(term =>
} this.termAssignments[term].filter(a => a.graded).length != 0);
})
// Specific time
return this.computed.termAssignments[timeCode];
} }
private _cacheAssignments: Assignment[]; /**
* 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 graded assignments * Get assignments of the selected grading periods
*/ */
get assignments(): Assignment[] get assignments(): Assignment[]
{ {
if (this._cacheAssignments == null) return this.gradingPeriods
this._cacheAssignments = this.rawSelectedAssignments.filter(a => a.complete == 'Complete'); .flatMap(term => this.termAssignments[term])
.filter(a => a.graded)
return this._cacheAssignments; .sort((a, b) => b.time - a.time);
} }
private _cacheLetterGrade: string; /**
* 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 letter grade
*/ */
get letterGrade(): string get letterGrade(): string
{ {
if (this._cacheLetterGrade == null) return this.cache.get('LetterGrade', () =>
{ {
// Get scale // Get scale
let scale = GPAUtils.findScale(this.numericGrade); let scale = GPAUtils.findScale(this.numericGrade);
// Scale not found // Scale not found
if (scale == undefined) return this._cacheLetterGrade = '--'; return scale == undefined ? '--' : scale.letter;
})
// Cache
this._cacheLetterGrade = scale.letter;
}
return this._cacheLetterGrade;
} }
private _cacheNumericGrade: number; /**
* 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 numeric grade
*/ */
get numericGrade(): number get numericGrade()
{ {
// Cached return this.cache.get('NumericGrade', () =>
if (this._cacheNumericGrade == null)
{ {
// Calculate return this.gradingPeriods.map(term => this.numericGradeTerm(term))
if (this.grading.method == 'PERCENT_TYPE') .reduce((p, v) => p + v) / this.gradingPeriods.length
{ })
this._cacheNumericGrade = GPAUtils.getPercentTypeAverage(this, this.assignments);
}
else if (this.grading.method == 'TOTAL_MEAN')
{
this._cacheNumericGrade = GPAUtils.getTotalMeanAverage(this.assignments);
}
else this._cacheNumericGrade = -1;
}
return this._cacheNumericGrade;
} }
private _cacheAssignmentTypes: AssignmentType[]; /**
* 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 assignment types
*/ */
get assignmentTypes(): AssignmentType[] get assignmentTypes(): AssignmentType[]
{ {
if (this._cacheAssignmentTypes == null) return this.cache.get('AssignmentTypes', () =>
{ {
// Get all types // Get all types
let types = this.assignments.map(a => a.type); let types = this.assignments.map(a => a.type);
@@ -243,7 +371,7 @@ export default class Course
let totalScoreMax = this.assignments.reduce((sum, a) => sum + a.scoreMax, 0); let totalScoreMax = this.assignments.reduce((sum, a) => sum + a.scoreMax, 0);
// For every type... // For every type...
this._cacheAssignmentTypes = types.map(type => return types.map(type =>
{ {
// Get assignments of the type // Get assignments of the type
let typeAssignments = this.assignments.filter(a => a.type == type); let typeAssignments = this.assignments.filter(a => a.type == type);
@@ -253,16 +381,38 @@ export default class Course
let scoreMax = typeAssignments.reduce((sum, a) => sum + a.scoreMax, 0); let scoreMax = typeAssignments.reduce((sum, a) => sum + a.scoreMax, 0);
// Calculate weight // Calculate weight
let weight = this.grading.method == 'PERCENT_TYPE' let weight = this.termGrading[0].method == 'PERCENT_TYPE'
? this.grading.weightingMap[type] : scoreMax / totalScoreMax; ? this.termGrading[0].weightingMap[type] : scoreMax / totalScoreMax;
// Return // Return
return {name: type, id: typeAssignments[0].typeId, weight: +(weight * 100).toFixed(2), return {name: type, id: typeAssignments[0].typeId, weight: +(weight * 100).toFixed(2),
scoreMax: scoreMax, score: score, percent: +(score / scoreMax * 100).toFixed(2), scoreMax: scoreMax, score: score, percent: +(score / scoreMax * 100).toFixed(2),
assignmentCount: typeAssignments.length} assignmentCount: typeAssignments.length}
}) })
} })
}
return this._cacheAssignmentTypes; /**
* 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);
}
}
+3 -15
View File
@@ -1,4 +1,3 @@
import Course from '@/logic/course';
import Navigation from '@/components/navigation/navigation'; import Navigation from '@/components/navigation/navigation';
import Constants from '@/constants'; import Constants from '@/constants';
@@ -20,17 +19,6 @@ UNKNOWN_COURSE_LIST.set('Painting', LEVEL_CP);
export class CourseUtils export class CourseUtils
{ {
/**
* Format course to tab index string
*
* @param course Course object
* @return string Tab index
*/
public static formatTabIndex(course: Course): string
{
return `course/${course.id}/${course.name.toLowerCase().split(' ').join('-')}`;
}
/** /**
* Detect course level based on course name * Detect course level based on course name
* *
@@ -72,7 +60,7 @@ export class CourseUtils
*/ */
static getTermBeginDate() static getTermBeginDate()
{ {
let selected = Navigation.instance.getSelectedGradingPeriod(); let selected = Navigation.instance.getSelectedTerm();
return selected == -1 ? Constants.TERMS[0] : Constants.TERMS[selected]; return selected == -1 ? Constants.TERMS[0] : Constants.TERMS[selected];
} }
@@ -82,8 +70,8 @@ export class CourseUtils
*/ */
static getTermEndDate() static getTermEndDate()
{ {
let selected = Navigation.instance.getSelectedGradingPeriod(); let selected = Navigation.instance.getSelectedTerm();
return selected == -1 ? Constants.TERMS[3] : Constants.TERMS[selected + 1]; return selected == -1 ? Constants.TERMS[4] : Constants.TERMS[selected + 1];
} }
} }
-2
View File
@@ -1,5 +1,3 @@
import moment from 'moment';
export class FormatUtils export class FormatUtils
{ {
/** /**
+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;
}
+9 -10
View File
@@ -1,4 +1,4 @@
import Course, {Assignment} from '@/logic/course'; import Course, {Assignment, Grading} from '@/logic/course';
export interface Scale export interface Scale
{ {
@@ -48,7 +48,7 @@ export class GPAUtils
{ {
accurate = false; accurate = false;
} }
else if (course.level != 'none') else if (course.level != 'none' && !isNaN(course.numericGrade))
{ {
courses.push(course); courses.push(course);
} }
@@ -65,7 +65,7 @@ export class GPAUtils
let maxTotal = 0; let maxTotal = 0;
courses.forEach(course => courses.forEach(course =>
{ {
totalGPA += this.getGP(course, course.letterGrade); totalGPA += this.getGP(course, course.numericGrade);
maxTotal += this.getGP(course, 'A+'); maxTotal += this.getGP(course, 'A+');
}); });
@@ -83,7 +83,7 @@ export class GPAUtils
* @param course Course * @param course Course
* @param letterGrade Letter grade * @param letterGrade Letter grade
*/ */
public static getGP(course: Course, letterGrade: string): number public static getGP(course: Course, letterGrade: string | number): number
{ {
// Get scale // Get scale
let scale = this.findScale(letterGrade); let scale = this.findScale(letterGrade);
@@ -140,12 +140,11 @@ export class GPAUtils
/** /**
* Calculate the percent type * Calculate the percent type
* TODO: Combine it with overall-line
* *
* @param course * @param grading
* @param assignments * @param assignments
*/ */
public static getPercentTypeAverage(course: Course, assignments: Assignment[]) public static getPercentTypeAverage(grading: Grading, assignments: Assignment[])
{ {
let typeScores: {[index: string]: any} = {}; let typeScores: {[index: string]: any} = {};
let typeCounts: {[index: string]: any} = {}; let typeCounts: {[index: string]: any} = {};
@@ -168,11 +167,11 @@ export class GPAUtils
// Eg. If HW = 25% and Quiz = 75%, I have 1 hw and 0 quiz // Eg. If HW = 25% and Quiz = 75%, I have 1 hw and 0 quiz
// Without total percentage, the avg grade I get is 25%. // Without total percentage, the avg grade I get is 25%.
let totalPercentage = 0; let totalPercentage = 0;
for (let type in course.grading.weightingMap) for (let type in grading.weightingMap)
{ {
if (typeScores[type] != undefined) if (typeScores[type] != undefined)
{ {
totalPercentage += course.grading.weightingMap[type]; totalPercentage += grading.weightingMap[type];
} }
} }
@@ -180,7 +179,7 @@ export class GPAUtils
let score = 0; let score = 0;
for (let type in typeScores) for (let type in typeScores)
{ {
let typeFactor = course.grading.weightingMap[type] / totalPercentage; let typeFactor = grading.weightingMap[type] / totalPercentage;
score += typeScores[type] * typeFactor / typeCounts[type]; score += typeScores[type] * typeFactor / typeCounts[type];
} }
+4
View File
@@ -1,4 +1,5 @@
import Constants from '@/constants'; import Constants from '@/constants';
import App from '@/components/app/app';
export default class GraphUtils export default class GraphUtils
{ {
@@ -54,6 +55,9 @@ export default class GraphUtils
*/ */
static getGradeMarkAreas(opacity: number) static getGradeMarkAreas(opacity: number)
{ {
// TODO: Auto update after switching dark mode (possibly by refreshing)
opacity = App.instance.darkMode ? 0.1 : opacity;
return { return {
silent: true, silent: true,
data: data:
+3 -2
View File
@@ -1,13 +1,14 @@
import Constants from '@/constants'; import Constants from '@/constants';
import LoginUser from '@/logic/login-user';
export class HttpUtils export class HttpUtils
{ {
public token: string = ''; public user: LoginUser;
public post(node: string, body: any): Promise<any> public post(node: string, body: any): Promise<any>
{ {
// Add token // Add token
if (this.token != '') body['token'] = this.token; if (this.user != null) body['token'] = this.user.token;
// Create promise // Create promise
return new Promise<any>((resolve, reject) => return new Promise<any>((resolve, reject) =>
-35
View File
@@ -1,35 +0,0 @@
import {Assignment} from '@/logic/course';
export default class JsonUtils
{
/**
* This method filters the information provided in an assignments json.
*
* @param assignments Assignments object
* @returns Assignment[] Filtered assignment grade object list
*/
public static filterAssignments(assignments: any): Assignment[]
{
return assignments.assignments.map((assignment: any) =>
{
return {
id: assignment.assignment_id,
scoreId: assignment.score_id,
type: assignment.assignment_type,
typeId: assignment.assignment_type_id,
description: assignment.assignment_description,
time: new Date(assignment._date).getTime(),
complete: assignment.completion_status,
include: assignment.include_in_calculated_grade == 1,
display: assignment.display_grade == 1,
unread: assignment.is_unread == 1,
scoreMax: assignment.maximum_score,
score: +assignment.raw_score,
gradingPeriod: +assignment.grading_period.replace('Quarter ', '') - 1
}
});
}
}
+5 -5
View File
@@ -1,12 +1,12 @@
<template> <template>
<el-card id="course-card" class="course-card"> <el-card id="course-card" class="course-card">
<course-head :clickable="false" :course="course" :unread="countUnread()"></course-head> <course-head :clickable="false" :course="course" :unread="countUnread()"/>
<div class="course-card-content expand"> <div class="course-card-content expand">
<el-row> <el-row>
<el-col :span="24" class="course-page-graph"> <el-col :span="24" class="course-page-graph">
<el-card class="large overall-line-card vertical-center"> <el-card class="large overall-line-card vertical-center">
<course-scatter :course="course"></course-scatter> <course-scatter :course="course"/>
</el-card> </el-card>
</el-col> </el-col>
</el-row> </el-row>
@@ -15,13 +15,13 @@
<el-col :span="12" class="course-page-graph"> <el-col :span="12" class="course-page-graph">
<el-card class="large overall-line-card vertical-center type-graph" <el-card class="large overall-line-card vertical-center type-graph"
body-style="padding: 0"> body-style="padding: 0">
<TypeRadar :course="course"></TypeRadar> <TypeRadar :course="course"/>
</el-card> </el-card>
</el-col> </el-col>
<el-col :span="12" class="course-page-graph"> <el-col :span="12" class="course-page-graph">
<el-card class="large overall-line-card vertical-center type-graph" <el-card class="large overall-line-card vertical-center type-graph"
body-style="padding: 0"> body-style="padding: 0">
<TypePie :course="course"></TypePie> <TypePie :course="course"/>
</el-card> </el-card>
</el-col> </el-col>
</el-row> </el-row>
@@ -68,4 +68,4 @@
} }
</script> </script>
<style src="./course-page.scss" lang="scss" scoped></style> <style src="./course-page.scss" lang="scss" scoped/>
@@ -6,8 +6,7 @@ import Course, {Assignment} from '@/logic/course';
import GraphUtils from '@/logic/utils/graph-utils'; import GraphUtils from '@/logic/utils/graph-utils';
import chroma from 'chroma-js'; import chroma from 'chroma-js';
@Component({ @Component
})
export default class CourseScatter extends Vue export default class CourseScatter extends Vue
{ {
@Prop({required: true}) course: Course; @Prop({required: true}) course: Course;
@@ -70,8 +69,8 @@ export default class CourseScatter extends Vue
type: 'cross' type: 'cross'
}, },
formatter: (ps: any[]) => moment(ps[0].data[0]).format('MMM DD, YYYY') + '<br>' + ps.map(p => formatter: (ps: any[]) => moment(ps[0].data[0]).format('MMM DD, YYYY') + '<br>' + ps.map(p =>
`${GraphUtils.DOT.replace('{color}', p.color)} `${GraphUtils.DOT.replace('{color}', p.color.colorStops[1].color)}
${FormatUtils.limit(p.data[2].description, 22)}: ${p.data[1]}%<br>`).join('') ${FormatUtils.limit(p.data[2].description, 22)}: ${p.data[1]}%<br>`).join('')
}, },
// Legend // Legend
@@ -1,8 +1,7 @@
<template> <template>
<div id="course-scatter"> <div id="course-scatter">
<ve-scatter height="450px" class="graph" :extend="{a: this.course.name}" :after-config="afterConfig"></ve-scatter> <ve-scatter height="450px" class="graph" :extend="{a: this.course.name}" :after-config="afterConfig"/>
</div> </div>
</template> </template>
<script src="./course-scatter.ts" lang="ts"></script> <script src="./course-scatter.ts" lang="ts"></script>
<style lang="scss" scoped></style>
+1 -5
View File
@@ -1,8 +1,6 @@
import {Component, Prop, Vue} from 'vue-property-decorator'; import {Component, Prop, Vue} from 'vue-property-decorator';
import Constants from '@/constants'; import Constants from '@/constants';
import {FormatUtils} from '@/logic/utils/format-utils'; import Course from '@/logic/course';
import moment from 'moment';
import Course, {Assignment} from '@/logic/course';
import GraphUtils from '@/logic/utils/graph-utils'; import GraphUtils from '@/logic/utils/graph-utils';
@Component @Component
@@ -31,8 +29,6 @@ export default class TypePie extends Vue
...GraphUtils.getBaseSettings('Assignment Type Weight', ...GraphUtils.getBaseSettings('Assignment Type Weight',
'How much each type of assignment affect your average'), 'How much each type of assignment affect your average'),
tooltip: GraphUtils.tooltipCssShadow(),
// Data // Data
series: series:
{ {
+1 -5
View File
@@ -1,8 +1,6 @@
import {Component, Prop, Vue} from 'vue-property-decorator'; import {Component, Prop, Vue} from 'vue-property-decorator';
import Constants from '@/constants'; import Constants from '@/constants';
import {FormatUtils} from '@/logic/utils/format-utils'; import Course from '@/logic/course';
import moment from 'moment';
import Course, {Assignment} from '@/logic/course';
import GraphUtils from '@/logic/utils/graph-utils'; import GraphUtils from '@/logic/utils/graph-utils';
@Component @Component
@@ -66,8 +64,6 @@ export default class TypeRadar extends Vue
center: ['50%', '55%'] center: ['50%', '55%']
}, },
tooltip: GraphUtils.tooltipCssShadow(),
// Data // Data
series: series:
{ {
+1 -2
View File
@@ -1,8 +1,7 @@
<template> <template>
<div id="type-radar"> <div id="type-radar">
<ve-radar height="420px" class="graph" :extend="{a: this.course.name}" :after-config="afterConfig"></ve-radar> <ve-radar height="420px" class="graph" :extend="{a: this.course.name}" :after-config="afterConfig"/>
</div> </div>
</template> </template>
<script src="./type-radar.ts" lang="ts"></script> <script src="./type-radar.ts" lang="ts"></script>
<style lang="scss" scoped></style>
+1 -2
View File
@@ -4,8 +4,7 @@ import {GPAUtils} from '@/logic/utils/gpa-utils';
import Constants from '@/constants'; import Constants from '@/constants';
import {FormatUtils} from '@/logic/utils/format-utils'; import {FormatUtils} from '@/logic/utils/format-utils';
@Component({ @Component
})
export default class OverallBar extends Vue export default class OverallBar extends Vue
{ {
@Prop({required: true}) courses: Course[]; @Prop({required: true}) courses: Course[];
@@ -1,7 +1,6 @@
<template> <template>
<div id="overall-bar"> <div id="overall-bar">
<ve-bar height="450px" class="graph" <ve-bar height="450px" class="graph" :extend="chartSettings"/>
:extend="chartSettings"></ve-bar>
</div> </div>
</template> </template>
@@ -57,6 +57,13 @@
text-align: right; text-align: right;
float: right; float: right;
// Status / Problems
span.status
{
margin-right: 8px;
}
// Percentage score
span.percent span.percent
{ {
font-style: italic; font-style: italic;
@@ -18,6 +18,9 @@
</el-col> </el-col>
<el-col :span="6" class="grade"> <el-col :span="6" class="grade">
<span v-if="assignment.problem" class="status entry-box" :style="{color: assignment.problemColor}">
{{assignment.problem}}
</span>
<span class="percent entry-box"> <span class="percent entry-box">
{{(assignment.score / assignment.scoreMax * 100).toFixed(1)}} {{(assignment.score / assignment.scoreMax * 100).toFixed(1)}}
<span class="symbol">%</span> <span class="symbol">%</span>
@@ -68,4 +71,4 @@
} }
} }
</script> </script>
<style src="./assignment-entry.scss" lang="scss" scoped></style> <style src="./assignment-entry.scss" lang="scss" scoped/>
@@ -1,27 +1,30 @@
// Main card content // Main card content
.course-card-content.main .course-card-content.main
{ {
padding: 0 20px 0 20px;
height: 90px;
// Main color // Main color
background: white; background: white;
// Alignment
display: block;
padding: 20px;
height: 50px;
} }
.course-col-name #block-info
{ {
// Align left // Align left
text-align: left; text-align: left;
float: left; float: left;
.course-name #name
{ {
overflow: hidden; overflow: hidden;
font-size: 22px; font-size: 22px;
color: var(--main); color: var(--main);
} }
.course-teacher #teacher
{ {
font-size: 12px; font-size: 12px;
color: #999999; color: #999999;
@@ -29,7 +32,7 @@
} }
} }
.course-col-grade #block-grade
{ {
// Align right // Align right
text-align: right; text-align: right;
@@ -37,17 +40,18 @@
// Adjust position // Adjust position
margin-top: -2px; margin-top: -2px;
margin-left: 10px;
.course-grade #grade
{ {
font-size: 21px; font-size: 21px;
} }
.course-updates #updates
{ {
font-size: 14px; font-size: 14px;
.unread-number #unread-number
{ {
display: inline-block; display: inline-block;
width: 20px; width: 20px;
@@ -60,33 +64,53 @@
margin-right: 3px; margin-right: 3px;
} }
.unread-text #unread-text
{ {
font-style: italic; font-style: italic;
} }
} }
.course-updates.unread #updates.unread
{ {
.unread-number #unread-number
{ {
background: var(--unread); background: var(--unread);
color: white; color: white;
} }
.unread-text #unread-text
{ {
color: var(--unread); color: var(--unread);
} }
} }
.course-updates.none #updates.none
{ {
color: #999999; color: #999999;
#unread-number
.unread-number
{ {
background: #eeeeee; 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,33 +1,34 @@
<template> <template>
<div id="course-head" class="course-card-content main vertical-center" @click="redirect"> <div id="course-head" class="course-card-content main vertical-center"
<el-row> :class="clickable ? 'clickable' : ''" @click="redirect">
<el-col :span="12" class="course-col-name"> <div id="block-info">
<div v-if="clickable" class="course-name clickable" @click="redirect">{{course.name}}</div> <div id="name">{{course.name}}</div>
<div v-if="!clickable" class="course-name">{{course.name}}</div> <div id="teacher">{{course.teacherName}}</div>
<div class="course-teacher">{{course.teacherName}}</div> </div>
</el-col> <div id="block-grade">
<el-col :span="12" class="course-col-grade"> <div id="grade">
<div class="course-grade"> <span id="letter">{{course.letterGrade}} </span>
<span class="letter">{{course.letterGrade}} </span> <span id="numeric">{{course.numericGrade === undefined ? '--' : course.numericGrade.toFixed(2)}}</span>
<span class="numeric">{{course.numericGrade === undefined ? '--' : course.numericGrade.toFixed(2)}}</span> <span id="percent" v-if="course.numericGrade !== undefined">%</span>
<span class="percent" v-if="course.numericGrade !== undefined">%</span> </div>
</div> <div id="updates" @click="redirect" :class="unread === 0 ? 'none' : 'unread'">
<div class="course-updates" @click="redirect" :class="unread === 0 ? 'none' : 'unread'"> <span id="unread-number">{{unread}}</span>
<span class="unread-number">{{unread}}</span> <span id="unread-text" :class="clickable ? 'clickable' : ''">new update{{unread >= 2 ? 's' : ''}}</span>
<span class="unread-text" :class="clickable ? 'clickable' : ''"> </div>
new update{{unread >= 2 ? 's' : ''}} </div>
</span> <div id="block-term-grades" v-if="course.rawSelectedTerm === -1"
</div> v-for="term in course.allGradingPeriods.slice().reverse()">
</el-col> <div id="term">Term {{term + 1}}</div>
</el-row> <div id="term-letter">{{course.letterGradeTerm(term)}}</div>
<div id="term-numeric">{{course.numericGradeTerm(term).toFixed(1)}}%</div>
</div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import {Component, Prop, Vue} from 'vue-property-decorator'; import {Component, Prop, Vue} from 'vue-property-decorator';
import Course from '@/logic/course'; import Course from '@/logic/course';
import {CourseUtils} from '@/logic/utils/course-utils'; import App from '@/components/app/app';
import Navigation from '@/components/navigation/navigation';
@Component @Component
export default class CourseHead extends Vue export default class CourseHead extends Vue
@@ -44,9 +45,9 @@
redirect() redirect()
{ {
if (!this.clickable) return; if (!this.clickable) return;
Navigation.instance.updateIndex(CourseUtils.formatTabIndex(this.course)); App.instance.nav.updateIndex(this.course.urlIndex);
} }
} }
</script> </script>
<style src="./course-head.scss" lang="scss"></style> <style src="./course-head.scss" lang="scss" scoped/>
@@ -1,51 +0,0 @@
import {Component, Prop, Vue} from 'vue-property-decorator';
import App from '@/components/app/app';
import AssignmentEntry from '@/pages/overall/overall-course/assignment-entry/assignment-entry.vue';
import CourseHead from '@/pages/overall/overall-course/course-head/course-head.vue';
import Course, {Assignment} from '@/logic/course';
@Component({
components: {UnreadEntry: AssignmentEntry, CourseHead}
})
export default class OverallCourse extends Vue
{
@Prop({required: true}) course: Course;
private unread: number = -1;
private unreadAssignments: Assignment[] = [];
/**
* Count the number of unread assignments with cache
*/
countUnread(): number
{
if (this.unread == -1)
{
this.unreadAssignments = this.course.assignments.filter(a => a.unread);
return this.unread = this.unreadAssignments.length;
}
else return this.unread;
}
/**
* Mark an assignment as read
*/
markAsRead(assignment: Assignment)
{
App.http.post('/mark-as-read', {scoreId: assignment.scoreId})
.then(response =>
{
// Check success
if (response.success)
{
this.unreadAssignments = this.unreadAssignments.filter(a => a != assignment);
this.unread = this.unreadAssignments.length;
}
else
{
// Show error message TODO: Show it properly
alert(response.data)
}
})
}
}
@@ -1,19 +1,47 @@
<template> <template>
<div id="overall-course"> <div id="overall-course">
<el-card class="course-card"> <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" <div class="course-card-content expand"
v-if="countUnread() !== 0"> v-if="unread() !== 0">
<unread-entry v-for="assignment in unreadAssignments" <assignment-entry v-for="assignment in unreadAssignments()"
:assignment="assignment" :assignment="assignment"
:key="assignment.id" :key="assignment.id"
unread="true" unread="true"
v-on:mark-as-read="markAsRead"> v-on:mark-as-read="assignment.markAsRead()">
</unread-entry> </assignment-entry>
</div> </div>
</el-card> </el-card>
</div> </div>
</template> </template>
<script src="./overall-course.ts" lang="ts"></script> <script lang="ts">
<style src="./overall-course.scss" lang="scss" scoped></style> 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/>
+37 -23
View File
@@ -1,11 +1,11 @@
import {Component, Prop, Vue} from 'vue-property-decorator'; import {Component, Prop, Vue} from 'vue-property-decorator';
import moment from 'moment'; import moment from 'moment';
import Course, {Assignment} from '@/logic/course'; import Course from '@/logic/course';
import Constants from '@/constants';
import Navigation from '@/components/navigation/navigation';
import {CourseUtils} from '@/logic/utils/course-utils'; import {CourseUtils} from '@/logic/utils/course-utils';
import GraphUtils from '@/logic/utils/graph-utils'; import GraphUtils from '@/logic/utils/graph-utils';
import {GPAUtils} from '@/logic/utils/gpa-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 export default class OverallLine extends Vue
@@ -33,8 +33,7 @@ export default class OverallLine extends Vue
[ [
{ {
type: 'slider', type: 'slider',
startValue: Math.max(moment().subtract(30, 'days').toDate().getTime(), startValue: this.getStartDate(),
CourseUtils.getTermBeginDate().getTime()),
// Minimum zoom: 1 week // Minimum zoom: 1 week
minValueSpan: 7 * 24 * 60 * 60 * 1000 minValueSpan: 7 * 24 * 60 * 60 * 1000
@@ -83,6 +82,20 @@ export default class OverallLine extends Vue
return this.settings; 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 * Generate series data
*/ */
@@ -118,16 +131,15 @@ export default class OverallLine extends Vue
type: 'line', type: 'line',
smooth: true, smooth: true,
symbol: 'circle', // circle, diamond, emptyCircle, none symbol: 'circle', // circle, diamond, emptyCircle, none
data: this.toDateRange([...assignments.map(a => a.time)].map((time, i) => data: this.toDateRange([...new Set(assignments.map(a => a.time))].map(time =>
{ {
// Find subset before this assignment // Find subset before this assignment
let subset = assignments.filter(a => a.time <= time); let subset = course.getAssignmentsBefore(time);
// Find grade // Find grade
if (course.grading.method == 'PERCENT_TYPE') if (course.termGrading[subset.term].method == 'PERCENT_TYPE')
return [time, GPAUtils.getPercentTypeAverage(course, subset)]; return [time, GPAUtils.getPercentTypeAverage(course.termGrading[subset.term], subset.assignments)];
if (course.grading.method == 'TOTAL_MEAN') else return [time, GPAUtils.getTotalMeanAverage(subset.assignments)];
return [time, GPAUtils.getTotalMeanAverage(subset)];
})) }))
} }
} }
@@ -141,26 +153,28 @@ export default class OverallLine extends Vue
private toDateRange(data: any[]) private toDateRange(data: any[])
{ {
// Find the min date // Find the min date
let minDates = this.courses.map(course => course.assignments[course.assignments.length - 1].time); let minDate: Date = new Date(data[0][0]);
let minDate: Date = new Date(Math.min.apply(null, minDates));
// Find the dates in between // Find the dates in between
let now = new Date(Math.min(new Date().getTime(), CourseUtils.getTermEndDate().getTime())); let now = new Date(Math.min(new Date().getTime(), CourseUtils.getTermEndDate().getTime()));
let dates: number[] = []; let times: number[] = [];
for (let date = minDate; date <= now; date.setDate(date.getDate() + 1)) for (let date = minDate; date <= now; date.setDate(date.getDate() + 1)) times.push(date.getTime());
{
dates.push(new Date(date).getTime());
}
// Map the points
let lastValue: any = null; let lastValue: any = null;
return dates.map(date => return times.map(time =>
{ {
// Data point on this specific date // Data point on this specific date
let thisValue = data.find(a => a[0] == date); let thisValue = data.find(a => a[0] == time);
// None // Switching terms
if (thisValue == null) return [date, lastValue == null ? null : lastValue[1]]; if (Constants.TERMS.find(t => t.getTime() == time))
else return [date, (lastValue = thisValue)[1]]; lastValue = null;
// Find value
return thisValue == null
? lastValue == null ? null : [time, lastValue[1]]
: [time, (lastValue = thisValue)[1]];
}); });
} }
} }
@@ -1,6 +1,6 @@
<template> <template>
<div id="overall-line"> <div id="overall-line">
<ve-line :extend="{a: this.courses}" :after-config="afterConfig"></ve-line> <ve-line :extend="{a: this.courses}" :after-config="afterConfig"/>
</div> </div>
</template> </template>
+7
View File
@@ -51,3 +51,10 @@
margin-right: 20px; margin-right: 20px;
min-width: 170px; min-width: 170px;
} }
.dialog-checkbox
{
display: block;
margin-top: 20px;
margin-bottom: -20px;
}
+82 -6
View File
@@ -1,5 +1,19 @@
<template> <template>
<div id="overall"> <div id="overall">
<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"
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-row v-if="getGPA().gpa !== -1">
<el-col :span="4" class="overall-span"> <el-col :span="4" class="overall-span">
<el-card class="large gpa-card vertical-center" body-style="padding: 0"> <el-card class="large gpa-card vertical-center" body-style="padding: 0">
@@ -15,12 +29,12 @@
</el-col> </el-col>
<el-col :span="14" class="overall-span"> <el-col :span="14" class="overall-span">
<el-card class="large overall-line-card vertical-center" body-style="padding: 0 10px"> <el-card class="large overall-line-card vertical-center" body-style="padding: 0 10px">
<overall-line :courses="courses"></overall-line> <overall-line :courses="courses"/>
</el-card> </el-card>
</el-col> </el-col>
<el-col :span="6" class="overall-span"> <el-col :span="6" class="overall-span">
<el-card class="large overall-bar-card vertical-center" body-style="padding: 0 10px"> <el-card class="large overall-bar-card vertical-center" body-style="padding: 0 10px">
<overall-bar :courses="courses"></overall-bar> <overall-bar :courses="courses"/>
</el-card> </el-card>
</el-col> </el-col>
</el-row> </el-row>
@@ -42,8 +56,8 @@
import {Component, Prop, Vue} from 'vue-property-decorator'; import {Component, Prop, Vue} from 'vue-property-decorator';
import OverallLine from '@/pages/overall/overall-line/overall-line'; import OverallLine from '@/pages/overall/overall-line/overall-line';
import OverallBar from '@/pages/overall/overall-bar/overall-bar'; import OverallBar from '@/pages/overall/overall-bar/overall-bar';
import OverallCourse from '@/pages/overall/overall-course/overall-course'; import OverallCourse from '@/pages/overall/overall-course/overall-course.vue';
import Course from '@/logic/course'; import Course, {Assignment} from '@/logic/course';
import {GPAUtils} from '@/logic/utils/gpa-utils'; import {GPAUtils} from '@/logic/utils/gpa-utils';
@Component({ @Component({
@@ -57,11 +71,73 @@
* This function is called to get gpa since I can't import another * This function is called to get gpa since I can't import another
* class in the Vue file. * class in the Vue file.
*/ */
public getGPA() getGPA()
{ {
return GPAUtils.getGPA(this.courses); 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);
}
// 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> </script>
<style src="./overall.scss" lang="scss" scoped></style> <style src="./overall.scss" lang="scss" scoped/>
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

+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>
+1 -3
View File
@@ -15,9 +15,7 @@
"webpack-env" "webpack-env"
], ],
"paths": { "paths": {
"@/*": [ "@/*": ["src/*"]
"src/*"
]
}, },
"lib": [ "lib": [
"esnext", "esnext",
+3 -1
View File
@@ -2,7 +2,9 @@
"defaultSeverity": "warning", "defaultSeverity": "warning",
"linterOptions": { "linterOptions": {
"exclude": [ "exclude": [
"node_modules/**" "node_modules/**",
"*.json",
"**/*.json"
] ]
}, },
"rules": { "rules": {