Compare commits

...

171 Commits

Author SHA1 Message Date
Hykilpikonna 8dbef09ec9 [U] Release v0.4.2.912 2019-11-07 22:29:01 -05:00
Hykilpikonna 73b71f56a5 [S] Disable text selecting 2019-11-07 22:26:43 -05:00
Hykilpikonna ce702405d0 [S] Adjust no-grade font size 2019-11-07 22:26:33 -05:00
Hykilpikonna 21744a1bef [F] Fix gpa -1 when no grade issue 2019-11-07 22:26:13 -05:00
Hykilpikonna 82064f2f02 [O] Optimize graph max value 2019-11-07 22:14:46 -05:00
Hykilpikonna adfebc8c44 [F] Fix term 1 displaying term 2 grades issue 2019-11-07 22:00:08 -05:00
Hykilpikonna 2f30e67671 [F] Fix course grade displayed as 0.00 issue 2019-11-07 21:53:16 -05:00
Hykilpikonna 0a80d534eb [O] Optimize imports 2019-11-07 21:48:40 -05:00
Hykilpikonna a77c495843 [S] Further divide marklines 2019-11-07 21:45:34 -05:00
Hykilpikonna 5503aff6b1 [S] Dim marklines a little bit 2019-11-07 21:40:16 -05:00
Hykilpikonna 7cc4567245 [O] Optimize color representation 2019-11-07 21:39:09 -05:00
Hykilpikonna e7f29ad0bf [+] Add mark area below 70 2019-11-07 21:35:45 -05:00
Hykilpikonna cdbd101428 [+] Add mark area from 70 to 80 2019-11-07 21:33:22 -05:00
Hykilpikonna da650ef16b [+] Add mark area from 100 to 80 2019-11-07 21:31:51 -05:00
Hykilpikonna 8abfdd7f8e [+] Add mark area above 100 2019-11-07 21:31:36 -05:00
Hykilpikonna 3c66f99363 [F] Limit graph max value to 120 2019-11-07 20:57:35 -05:00
Hykilpikonna 251f87a072 [F] Fix assignment type update problem 2019-11-06 23:09:32 -05:00
Hykilpikonna e15c95561c [U] Release v0.4.1.895 2019-11-06 22:34:08 -05:00
Hykilpikonna f4ed39401c [S] Emphesize the title more 2019-11-06 22:29:03 -05:00
Hykilpikonna c3fb998254 [S] Set font size 2019-11-06 22:25:03 -05:00
Hykilpikonna d9e9bcc731 [S] Align the average span to the left 2019-11-06 22:24:56 -05:00
Hykilpikonna 110ff6daec [S] Center the average span 2019-11-06 22:24:46 -05:00
Hykilpikonna 7d83344e73 [F] Calculate percent average instead of ratio 2019-11-06 22:19:29 -05:00
Hykilpikonna 06d93398fb [S] Disable unread in courses page 2019-11-06 22:18:44 -05:00
Hykilpikonna b05328888e [M] Rename unread-entry class to assignment-entry 2019-11-06 22:17:25 -05:00
Hykilpikonna 19598f4a10 [+] Add average span 2019-11-06 22:16:59 -05:00
Hykilpikonna cd7102a5df [+] create method to get type average 2019-11-06 22:16:52 -05:00
Hykilpikonna 9bf1f6b624 [O] Optimize assignments filtering 2019-11-06 21:54:41 -05:00
Hykilpikonna f1681ad405 [F] Fix 'selectedTime undefined' 2019-11-06 21:54:11 -05:00
Hykilpikonna e0a70a23b0 [F] Fix: ' Invalid handler for event "command": got undefined' 2019-11-06 21:53:40 -05:00
Hykilpikonna 677dfb491a [+] Parse grading period 2019-11-06 21:50:28 -05:00
Hykilpikonna 07a9b38c8d [+] Add grading period field to app.ts 2019-11-06 21:50:22 -05:00
Hykilpikonna 53b78c4a94 [M] Rename cookie name too 2019-11-06 21:50:06 -05:00
Hykilpikonna 2057f08e40 [M] Rename time selection to grading period 2019-11-06 21:49:17 -05:00
Hykilpikonna 7bc843aa9a [O] Optimize assignments loading with [].map() 2019-11-06 21:41:49 -05:00
Hykilpikonna 0a8855e020 [F] Fix self-referencing error 2019-11-06 21:37:13 -05:00
Hykilpikonna 445d07c5e4 [+] Encapsulate method to get numeric and letter grades 2019-11-06 21:35:34 -05:00
Hykilpikonna 125e7d14de [O] Change letterGrade and numericGrade to raw fields 2019-11-06 21:32:55 -05:00
Hykilpikonna 4917f5cb0c [O] Optimize getGP 2019-11-06 21:32:38 -05:00
Hykilpikonna 2504b37eb7 [O] Also support numeric grade 2019-11-06 21:30:15 -05:00
Hykilpikonna b0d3bf4bd2 [+] Encapsulate method to find scale 2019-11-06 21:28:27 -05:00
Hykilpikonna e640384fd8 [O] Optimize scale list 2019-11-06 21:27:30 -05:00
Hykilpikonna adf7af6b84 [+] Create interface for scale 2019-11-06 21:27:19 -05:00
Hykilpikonna beb4155aab [+] Compare numeric grade with current term grade 2019-11-06 21:17:31 -05:00
Hykilpikonna 3337766ae6 [+] Add current term constant 2019-11-06 21:17:11 -05:00
Hykilpikonna dc135dc78b [+] Encapsulate getPercentTypeAverage 2019-11-06 21:16:47 -05:00
Hykilpikonna b39c51beea [O] getTotalMeanAverage pass in assignments not course 2019-11-06 21:16:31 -05:00
Hykilpikonna 971339e49c [F] Fix assignments order issue 2019-11-06 20:59:12 -05:00
Hykilpikonna 9db0431e96 [O] Use caching to optimize graphs 2019-11-06 20:57:52 -05:00
Hykilpikonna d667f2ab34 [-] Remove temp string 2019-11-06 20:39:58 -05:00
Hykilpikonna 9b895a8fd7 [+] Reload directly when selected time is changed 2019-11-06 20:39:47 -05:00
Hykilpikonna 66d7e18a71 [F] Fix selectedTime initialization issue 2019-11-06 20:30:05 -05:00
Hykilpikonna 4643155908 [+] Add useless string for updating 2019-11-06 20:29:39 -05:00
Hykilpikonna c6c3303a99 [O] Optimize onclick method call 2019-11-06 20:02:41 -05:00
Hykilpikonna 1aea558c40 [+] Store selected time in cookies 2019-11-06 20:02:29 -05:00
Hykilpikonna 316c7e1f63 [F] Fix 'cannot getTime() of undefined' 2019-11-06 20:01:32 -05:00
Hykilpikonna 8364befc91 [F] Fix more type problems 2019-11-06 19:26:41 -05:00
Hykilpikonna 47c25d0e71 [F] Fix imports 2019-11-06 19:23:39 -05:00
Hykilpikonna 422d574c6c [F] Fix term display number offset issue 2019-11-06 19:14:14 -05:00
Hykilpikonna 3400c07e77 [+] Encapsulate method to get assignments of specific time 2019-11-06 19:13:45 -05:00
Hykilpikonna 79bb10b14e [M] Rename assignments to rawAssignments 2019-11-06 19:12:24 -05:00
Hykilpikonna e7563fcfb5 [+] Encapsulate method to get selected time code 2019-11-06 19:11:55 -05:00
Hykilpikonna 43f0046827 [+] Update selected time dynamically 2019-11-06 19:11:41 -05:00
Hykilpikonna ef424dd9e3 [O] Unify time names 2019-11-06 19:11:27 -05:00
Hykilpikonna 37902b6d1f [+] Display selected time dynamically 2019-11-06 19:11:13 -05:00
Hykilpikonna 92e692f69a [O] Remove getGradedCourses 2019-11-06 18:27:19 -05:00
Hykilpikonna b6e0e12cab [+] Add isGraded method to course 2019-11-06 18:25:48 -05:00
Hykilpikonna 6eb7e421e0 [M] Move level detection to course 2019-11-06 18:22:35 -05:00
Hykilpikonna fbb9352546 [O] Move name parsing to course 2019-11-06 18:15:54 -05:00
Hykilpikonna 6f5c4f3a09 [M] Move loadAssignments to course 2019-11-06 18:13:24 -05:00
Hykilpikonna 1e91cec8d2 [O] Create course objects after parsing json 2019-11-06 18:08:47 -05:00
Hykilpikonna 17262e12af [+] Create constructor 2019-11-06 18:05:53 -05:00
Hykilpikonna 56a954c235 [O] Separate course object 2019-11-06 18:03:27 -05:00
Hykilpikonna da5eced769 [F] Fix pushed data duplicate issue 2019-11-06 17:55:10 -05:00
Hykilpikonna 71f2eccab4 [F] Fix filter assignment order issue 2019-11-06 17:54:57 -05:00
Hykilpikonna a5162c1f5b [O] Optimize date usage 2019-11-06 17:38:53 -05:00
Hykilpikonna 53c82fd477 [F] Fix sorting 2019-11-06 17:38:36 -05:00
Hykilpikonna 2867f8d09c [+] Set computed data 2019-11-06 17:26:52 -05:00
Hykilpikonna 5b902171c8 [+] Filter assignments into terms 2019-11-06 17:26:44 -05:00
Hykilpikonna c40c5b6b94 [+] Create computed model in course 2019-11-06 17:26:29 -05:00
Hykilpikonna b66c313b05 [+] Define terms date list 2019-11-06 17:20:09 -05:00
Hykilpikonna 48287cdc4b [O] Sort by date 2019-11-06 17:17:02 -05:00
Hykilpikonna 750c768848 [O] Use Date for date instead of string 2019-11-06 17:16:49 -05:00
Hykilpikonna 4b512d64d9 [+] Add commands to dropdown options 2019-11-06 16:48:43 -05:00
Hykilpikonna e121210e96 [S] Add proper up and down margins 2019-11-06 16:34:08 -05:00
Hykilpikonna 5a87608fa6 [S] Align dropdown to the right 2019-11-06 16:33:59 -05:00
Hykilpikonna e819abe789 [S] Make dropdown button smaller 2019-11-06 16:33:42 -05:00
Hykilpikonna eb718d14d1 [S] Add id to dropdown 2019-11-06 16:33:35 -05:00
Hykilpikonna 564896e940 [+] Create time selection button 2019-11-06 16:29:02 -05:00
Hykilpikonna d91c3875b0 [F] Fix bar graph color problem 2019-11-06 16:18:39 -05:00
Hykilpikonna 1fe2edb9f4 [F] Fix bar graph -1 problem 2019-11-06 16:17:31 -05:00
Hykilpikonna 91977e1226 [S] Change assignment type head font size 2019-11-06 16:06:33 -05:00
Hykilpikonna 948018c7de [F] Fix grade display when no grade exist 2019-11-06 16:06:03 -05:00
Hykilpikonna f2350680c8 [S] Optimize narrow height 2019-11-05 22:57:19 -05:00
Hykilpikonna 4cc424e079 [S] Make percentage score's width unified 2019-11-05 22:57:09 -05:00
Hykilpikonna 49d26fcf61 [S] Make month's width unified 2019-11-05 22:56:51 -05:00
Hykilpikonna 9fde3b21d4 [S] Fix margins when there are no unread 2019-11-05 22:51:47 -05:00
Hykilpikonna 6952649058 [S] Optimize date formatting 2019-11-05 22:51:15 -05:00
Hykilpikonna b12a717cea [S] Move unread property to css class 2019-11-05 22:41:33 -05:00
Hykilpikonna 8e1e222656 [S] Align the scores the same width 2019-11-05 22:39:57 -05:00
Hykilpikonna 8f7775bb69 [O] Reduce height for narrow layout 2019-11-05 22:33:22 -05:00
Hykilpikonna 0477161af2 [+] Create narrow property for class 2019-11-05 22:29:43 -05:00
Hykilpikonna 3b387db5b5 [S] Fix float left alignment issue 2019-11-05 22:24:45 -05:00
Hykilpikonna d85d0e4e36 [S] Align the text to the left 2019-11-05 22:23:25 -05:00
Hykilpikonna b74b086f98 [S] Vertically center the text 2019-11-05 22:23:14 -05:00
Hykilpikonna e1d20b822f [S] Optimize font size and color 2019-11-05 22:22:53 -05:00
Hykilpikonna e98997f230 [S] Specify background color 2019-11-05 18:25:56 -05:00
Hykilpikonna 82e17030d4 [+] Add property for background style 2019-11-05 18:22:44 -05:00
Hykilpikonna d1905ee2b1 [+] Create info div 2019-11-05 18:09:12 -05:00
Hykilpikonna 310b2bcee1 [S] Remove card body padding 2019-11-05 18:07:49 -05:00
Hykilpikonna d55eebce19 [F] Fix: Next course doesn't show up on mobile devices 2019-11-05 18:00:04 -05:00
Hykilpikonna 03507968ee [+] Add assignment entry components 2019-11-05 17:51:36 -05:00
Hykilpikonna 56769e4518 [F] Fix: Filter assignments based on value of type 2019-11-05 17:51:22 -05:00
Hykilpikonna fba589d6fe [+] Display type name 2019-11-05 17:44:16 -05:00
Hykilpikonna ff5b22b5dc [+] Filter assignment type 2019-11-05 17:42:46 -05:00
Hykilpikonna 14de2e0b23 [+] Create assignments property 2019-11-05 17:40:50 -05:00
Hykilpikonna 880e331c99 [F] Fix distinct key for v-for problem 2019-11-05 17:37:42 -05:00
Hykilpikonna cf794588df [-] Reomve duplicate type names 2019-11-05 17:37:29 -05:00
Hykilpikonna d362598d83 [+] Show an assignment type for each type 2019-11-05 17:29:27 -05:00
Hykilpikonna 536e98642d [+] Encapsulate method to get assignment types 2019-11-05 17:28:28 -05:00
Hykilpikonna baae0d088c [+] Add assignment head element in course page 2019-11-05 17:28:18 -05:00
Hykilpikonna 202feb12d7 [+] Create assignment type head vue comp 2019-11-05 17:22:13 -05:00
Hykilpikonna d2ab7c059f [F] Fix clear cookie issue 2019-11-04 16:23:40 -05:00
Hykilpikonna e1ef117dcd [S] Make logo non-clickable 2019-11-03 11:18:35 -05:00
Hykilpikonna fb3186d575 [S] Add shadow effect to logo 2019-11-03 11:18:26 -05:00
Hykilpikonna b913873951 [S] Adjust margin font size 2019-11-03 11:18:01 -05:00
Hykilpikonna bbdcb42316 [S] Adjust title margin size 2019-11-03 11:17:53 -05:00
Hykilpikonna 5438637224 [S] Add the title 2019-11-03 11:17:33 -05:00
Hykilpikonna 970a058ba3 [U] Pre-release v0.3.6.784 2019-11-03 10:44:57 -05:00
Hykilpikonna 5d295db1b7 [O] Add todo: update token each access 2019-11-03 10:43:50 -05:00
Hykilpikonna b22aac7ca2 [F] Fix cookie expiration issue 2019-11-03 10:26:10 -05:00
Hykilpikonna eef29a4611 [U] Add 'Unknown' to graded courses detection 2019-11-02 23:10:58 -04:00
Hykilpikonna 131952ed37 [O] Trim course names 2019-11-02 23:07:13 -04:00
Hykilpikonna 2e95c2550e [+] Create 404 redirection 2019-11-02 23:04:05 -04:00
Hykilpikonna 0db6e0d693 [S] Add some page end margins 2019-11-02 22:58:37 -04:00
Hykilpikonna 27198ad4e0 [U] Use CourseUtils.postProcess() 2019-11-02 22:57:41 -04:00
Hykilpikonna 3cbb6ebee6 [+] Add "HS" clubs to level detection 2019-11-02 22:56:27 -04:00
Hykilpikonna 2308df65b5 [+] Add post process for courses 2019-11-02 22:54:30 -04:00
Hykilpikonna c8f82cc991 [+] Use unknown course list 2019-11-02 22:54:02 -04:00
Hykilpikonna a6e1b905ed [O] Make unknown course list a map 2019-11-02 22:49:49 -04:00
Hykilpikonna 9432e9a806 [+] Create method to detect course level 2019-11-02 22:46:21 -04:00
Hykilpikonna d90280a10f [+] Create unknown course list 2019-11-02 22:46:04 -04:00
Hykilpikonna db30b7f807 [+] Create club constant 2019-11-02 22:45:52 -04:00
Hykilpikonna 20910b1562 [+] Add level constants 2019-11-02 22:18:58 -04:00
Hykilpikonna 5659a049e5 [O] Parse & in course names 2019-11-02 20:41:11 -04:00
Hykilpikonna 085812d859 [+] Create method to parse html text 2019-11-02 20:37:41 -04:00
Hykilpikonna e60a4669ac [F] Fix execution order problem 2019-11-02 20:21:26 -04:00
Hykilpikonna ac84907a98 [F] Update index only after loading 2019-11-02 20:20:09 -04:00
Hykilpikonna 04ee69e8e2 [F] Fix clickable required issue 2019-11-02 20:12:14 -04:00
Hykilpikonna a2445aca6a [F] Fix path reading issue 2019-11-02 20:11:39 -04:00
Hykilpikonna 336a58b23d [O] Move course head scss to separate file 2019-11-02 20:02:06 -04:00
Hykilpikonna 5a5cf9bd4d [O] Replace more ts-ignores with {required: true} 2019-11-02 20:00:19 -04:00
Hykilpikonna 44d262f457 [O] Inline course-page.ts 2019-11-02 19:58:25 -04:00
Hykilpikonna 27fda43373 [O] Inline overall.ts 2019-11-02 19:56:01 -04:00
Hykilpikonna b3de3b8405 [+] Only show mark as read if it is unread 2019-11-02 19:45:30 -04:00
Hykilpikonna 633918aa40 [U] Pass in unread value 2019-11-02 19:45:11 -04:00
Hykilpikonna 3bc59e87d3 [O] Inline assignment entry ts script 2019-11-02 19:41:55 -04:00
Hykilpikonna af7d9e9dca [+] Add unread prop to assignment entry 2019-11-02 19:40:26 -04:00
Hykilpikonna 3b8884dc84 [O] Optimize imports 2019-11-02 19:39:33 -04:00
Hykilpikonna 92158684c0 [-] Remove unnecessary ts-ignores 2019-11-02 19:38:45 -04:00
Hykilpikonna b37de9cf24 [O] Disable strict property initialization check 2019-11-02 19:37:13 -04:00
Hykilpikonna cd2dff5559 [M] Rename unread-entry to assignment-entry 2019-11-02 19:35:04 -04:00
Hykilpikonna d6a85af15c [O] Use relative date for starting value 2019-11-02 19:24:51 -04:00
Hykilpikonna 0d333879ca [O] Put login in a form 2019-11-02 19:08:02 -04:00
Hykilpikonna 905db3c73a [-] Remove debug logging 2019-11-02 19:04:56 -04:00
Hykilpikonna c5dfad8be8 [F] Remove trapezoid effect for mobile support 2019-11-02 19:03:35 -04:00
Hykilpikonna c9454a3832 [-] Remove todos that are done 2019-11-02 19:02:38 -04:00
Hykilpikonna c732d475f1 [O] Log when cookies doesn't exist 2019-10-23 20:20:54 -04:00
Hykilpikonna a4b7e0fd46 [O] Only clear cookies if cookies exists 2019-10-23 20:20:41 -04:00
Hykilpikonna bcee069b32 [F] Fix url index issue 2019-10-21 19:23:41 -04:00
Hykilpikonna 182208f8c3 [F] Fix Github Pages sub path issue by using # after url 2019-10-21 19:23:31 -04:00
37 changed files with 1020 additions and 462 deletions
+14
View File
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>404: Redirecting...</title>
<meta http-equiv = "refresh" content = "0; url = https://vera.hydev.org/" />
</head>
<body>
404 Not Found! Redirecting to (<a href="https://vera.hydev.org">https://vera.hydev.org</a>)...
<script>
window.location.href = 'https://vera.hydev.org';
</script>
</body>
</html>
+1
View File
@@ -5,6 +5,7 @@
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-bottom: 100px;
}
#app-content
+33 -47
View File
@@ -1,15 +1,14 @@
import {Component, Vue} from 'vue-property-decorator';
import Login from '@/components/login/login';
import Navigation from '@/components/navigation/navigation';
import Overall from '@/pages/overall/overall';
import Overall from '@/pages/overall/overall.vue';
import Constants from '@/constants';
import JsonUtils from '@/utils/json-utils';
import pWaitFor from 'p-wait-for';
import {HttpUtils} from '@/utils/http-utils';
import {CourseUtils} from '@/utils/course-utils';
import {GPAUtils} from '@/utils/gpa-utils';
import Loading from '@/components/loading/loading.vue';
import CoursePage from '@/pages/course/course-page';
import CoursePage from '@/pages/course/course-page.vue';
import Course from '@/logic/course';
/**
@@ -17,46 +16,22 @@ import CoursePage from '@/pages/course/course-page';
*/
export interface Assignment
{
id: number,
scoreId: number,
type: string,
typeId: number,
description: string,
date: string,
complete: string,
include: boolean,
display: boolean,
id: number
scoreId: number
type: string
typeId: number
description: string
date: Date
complete: string
include: boolean
display: boolean
unread: boolean,
unread: boolean
scoreMax: number,
scoreMax: number
score: number
}
/**
* A course
*/
export interface Course
{
assignmentsId: number,
id: number,
name: string,
teacherName: string,
status: string,
letterGrade?: string,
numericGrade?: number,
level: string,
scaleUp: number,
grading:
{
method: string,
weightingMap: {[index: string]: number}
}
assignments: Assignment[]
gradingPeriod: number
}
@Component({
@@ -144,7 +119,7 @@ export default class App extends Vue
if (response.success)
{
// Save courses
this.courses = response.data;
this.courses = response.data.map((courseJson: any) => new Course(courseJson));
// Load assignments
this.loadAssignments();
@@ -171,9 +146,7 @@ export default class App extends Vue
// Check success
if (response.success)
{
// Load assignments
// Parse json and filter it
course.assignments = JsonUtils.filterAssignments(response.data);
course.loadAssignments(response.data);
}
else throw new Error(response.data);
})
@@ -181,10 +154,10 @@ export default class App extends Vue
});
// Wait for assignments to be ready.
pWaitFor(() => this.courses.every(c => c.assignments != null)).then(() =>
pWaitFor(() => this.courses.every(c => c.rawAssignments != null)).then(() =>
{
// Filter courses
this.filteredCourses = CourseUtils.getGradedCourses(this.courses);
this.filteredCourses = this.courses.filter(c => c.isGraded);
// Check grading algorithms
this.checkGradingAlgorithms();
@@ -202,8 +175,10 @@ export default class App extends Vue
// Loop through all the courses
for (const course of this.filteredCourses)
{
let termGrade = +GPAUtils.getTotalMeanAverage(course.computed.termAssignments[Constants.CURRENT_TERM]).toFixed(2);
// Check if total-average grade is the same with percent-type grade
if (course.numericGrade == +GPAUtils.getTotalMeanAverage(course).toFixed(2))
if (course.rawNumericGrade == termGrade)
{
course.grading = {method: 'TOTAL_MEAN', weightingMap: {}};
}
@@ -268,4 +243,15 @@ export default class App extends Vue
// Refresh
window.location.reload();
}
/**
* Select time (Eg. Term 1, Term 2, All Year, etc.)
*
* @param code
*/
public selectTime(code: number)
{
// TODO: Optimize
window.location.reload();
}
}
+1 -1
View File
@@ -3,7 +3,7 @@
<login v-if="showLogin" v-on:login:token="onLogin"></login>
<navigation :courses="filteredCourses"
:activeIndex.sync="selectedTab"
v-on:sign-out="signOut">
@sign-out="signOut" @select-time="selectTime">
</navigation>
<div id="app-content" v-if="assignmentsReady && loading === ''">
+2 -4
View File
@@ -34,11 +34,9 @@
})
export default class Loading extends Vue
{
// @ts-ignore
@Prop() text: string;
@Prop({required: true}) text: string;
// @ts-ignore
@Prop() error: boolean;
@Prop({required: true}) error: boolean;
getText()
{
+20 -15
View File
@@ -1,12 +1,10 @@
import {Component, Prop, Vue} from 'vue-property-decorator';
import {Component, Vue} from 'vue-property-decorator';
import Constants from '@/constants';
import {HttpUtils} from '@/utils/http-utils';
import App from '@/components/app/app';
import VersionUtils from '@/utils/version-utils';
/**
* This component handles user login, and obtains data from the server.
* TODO: Press enter to login
*/
@Component({
components: {},
@@ -24,20 +22,27 @@ export default class Login extends Vue
*/
public created()
{
// Check cookies version
if (this.needToUpdateCookies())
{
console.log('Version Updated! Clearing cookies...');
// Clear all cookies
this.$cookies.keys().forEach(key => this.$cookies.remove(key));
}
// Check login cookies
if (this.$cookies.isKey('va.token'))
{
// Already contains valid token / TODO: Validate
this.$emit('login:token', this.$cookies.get('va.token'));
// Check cookies version
if (this.needToUpdateCookies())
{
console.log('Version Updated! Clearing cookies...');
// Clear all cookies
this.$cookies.keys().forEach(key => this.$cookies.remove(key));
}
else
{
// Already contains valid token / TODO: Validate
// TODO: Update token each access
this.$emit('login:token', this.$cookies.get('va.token'));
}
}
else
{
console.log('Cookies doesn\'t exist');
}
}
@@ -72,7 +77,7 @@ export default class Login extends Vue
{
// Save token to cookies
this.$cookies.set('va.token', response.data, '27d');
this.$cookies.set('va.version', Constants.VERSION);
this.$cookies.set('va.version', Constants.VERSION, '27d');
// Call custom event with token
this.$emit('login:token', response.data);
+15 -14
View File
@@ -5,23 +5,24 @@
<img alt="Vue logo" src="../../assets/logo.png">
<h1>Veracross Analyzer</h1>
<form id="login-form">
<el-input v-model="username"
placeholder="School Username"
:class="{'input-error': error !== ''}"
@keyup.enter.native="onEnter">
</el-input>
<el-input v-model="username"
placeholder="School Username"
:class="{'input-error': error !== ''}"
@keyup.enter.native="onEnter">
</el-input>
<el-input v-model="password"
placeholder="Veracross Password"
show-password=""
:class="{'input-error': error !== ''}"
@keyup.enter.native="onEnter">
</el-input>
<el-input v-model="password"
placeholder="Veracross Password"
show-password=""
:class="{'input-error': error !== ''}"
@keyup.enter.native="onEnter">
</el-input>
<div class="el-form-item__error custom">{{error}}</div>
<div class="el-form-item__error custom">{{error}}</div>
<el-button plain type="primary" @click="onLoginClick" :loading="loading">Login</el-button>
<el-button plain type="primary" @click="onLoginClick" :loading="loading">Login</el-button>
</form>
</div>
</div>
</div>
+29 -7
View File
@@ -27,6 +27,17 @@
width: 110px;
}
#nav-grading-period
{
// Float right
position: absolute;
right: 110px;
// Margins
margin-top: 12px;
margin-bottom: 12px;
}
#nav-title
{
// Float left
@@ -40,12 +51,21 @@
align-items: center;
// Margins
margin-left: 14px;
margin-left: 20px;
margin-right: 8px;
// Font
font-size: 1.25rem;
font: bold 16px arial;
display: inline-flex;
// Shadow effect
-webkit-background-clip: text;
background-color: #b1b1b1;
color: transparent;
text-shadow: 1px 1px 0 rgba(255, 255, 255, 0.85);
// Make it non-clickable
pointer-events: none;
}
#next-course
@@ -57,9 +77,14 @@
left: 25%;
padding-top: 2px;
box-shadow: 0 -2px 9px 0 #ecebeb;
}
// Trapezoid
transform: perspective(50px) rotateX(10deg);
footer
{
position: fixed;
left: 0;
bottom: 0;
width: 100%;
}
#prev-course
@@ -71,9 +96,6 @@
left: 25%;
padding-bottom: 2px;
box-shadow: 0 2px 9px 0 #ecebeb;
// Trapezoid
transform: perspective(50px) rotateX(-10deg);
}
.nav-course-operations
+46 -18
View File
@@ -1,8 +1,9 @@
import {Component, Prop, Vue} from 'vue-property-decorator';
import Constants from '@/constants';
import {Course} from '@/components/app/app';
import App from '@/components/app/app';
import {CourseUtils} from '@/utils/course-utils';
import {FormatUtils} from '@/utils/format-utils';
import pWaitFor from 'p-wait-for';
import Course from '@/logic/course';
/**
* This component is the top navigation bar
@@ -12,11 +13,11 @@ import {FormatUtils} from '@/utils/format-utils';
})
export default class Navigation extends Vue
{
// @ts-ignore
@Prop() activeIndex: string;
@Prop({required: true}) activeIndex: string;
// @ts-ignore
@Prop() courses: Course[];
@Prop({required: true}) courses: Course[];
private gradingPeriod: string = 'All Year';
// Instance
public static instance: Navigation;
@@ -25,17 +26,33 @@ export default class Navigation extends Vue
* This is called when the instance is created.
*/
public created()
{
// Check selected time
if (!this.$cookies.isKey('va.grading-period'))
{
this.$cookies.set('va.grading-period', this.gradingPeriod, '10y');
}
this.gradingPeriod = this.$cookies.get('va.grading-period');
}
/**
* This is called when the instance is loaded.
*/
public mounted()
{
// Set instance
Navigation.instance = this;
// Set history state
let url = window.location.pathname;
if (url == '/' || url == '') url = '/overall';
let url = '/' + window.location.hash;
if (url == '/' || url == '') url = '/#overall';
window.history.replaceState({lastTab: url.substring(1)}, '', url);
// Update initial index
this.updateIndex(url.substring(1), false);
// 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 =>
@@ -80,11 +97,8 @@ export default class Navigation extends Vue
// Record or not
if (history == null || history)
{
// Debug output TODO: Remove this
console.log(`onNavigate: Previous: ${this.activeIndex}, New: ${newIndex}`);
// Check url
let url = `/${newIndex}`;
let url = `/#${newIndex}`;
// Push history state
window.history.pushState({lastTab: newIndex}, '', url);
@@ -148,11 +162,25 @@ export default class Navigation extends Vue
}
/**
* This function is called when the sign out button is clicked.
* Select grading period
*
* @param command Term 1, Term 2, All Year, etc.
*/
public signOut()
public selectGradingPeriod(command: string)
{
// Call custom event
this.$emit('sign-out');
this.gradingPeriod = command;
this.$cookies.set('va.grading-period', command, '10y');
// Call event
this.$emit('select-time', this.getSelectedGradingPeriod());
}
/**
* Get code for selected time
*/
public getSelectedGradingPeriod(): number
{
if (this.gradingPeriod == 'All Year') return -1;
else return +this.gradingPeriod.replace('Term ', '') - 1;
}
}
+25 -7
View File
@@ -3,9 +3,9 @@
<el-menu style="margin-bottom: 10px;" class="centered" mode="horizontal"
:default-active="activeIndex" @select="onSelect">
<!--div id="nav-title">
<div id="nav-title">
Veracross Analyzer
</div-->
</div>
<el-menu-item index="overall">Overall</el-menu-item>
@@ -16,16 +16,34 @@
:key="course.name">{{course.name}}</el-menu-item>
</el-submenu>
<el-button @click="signOut" id="sign-out-button" type="text">Sign Out</el-button>
<!-- Grading period selection -->
<el-dropdown id="nav-grading-period" @command="selectGradingPeriod">
<el-button type="primary" size="medium">
{{gradingPeriod}}<i class="el-icon-arrow-down el-icon--right"></i>
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="Term 1">Term 1</el-dropdown-item>
<el-dropdown-item command="Term 2">Term 2</el-dropdown-item>
<el-dropdown-item command="Term 3" disabled>Term 3</el-dropdown-item>
<el-dropdown-item command="Term 4" disabled>Term 4</el-dropdown-item>
<el-dropdown-item command="All Year" divided>All Year</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<el-button @click="$emit('sign-out')" id="sign-out-button" type="text">Sign Out</el-button>
</el-menu>
<!-- Previous course / Next course (Only when the page is courses) -->
<div v-if="activeIndex.includes('course') && findNextCourse(-1) != null"
@click="nextCourse(-1)" id="prev-course" class="nav-course-operations unselectable">
PREVIOUS COURSE
</div>
<div v-if="activeIndex.includes('course') && findNextCourse(1) != null"
@click="nextCourse(1)" id="next-course" class="nav-course-operations unselectable">
NEXT COURSE
</div>
<footer>
<div v-if="activeIndex.includes('course') && findNextCourse(1) != null"
@click="nextCourse(1)" id="next-course" class="nav-course-operations unselectable">
NEXT COURSE
</div>
</footer>
</div>
</template>
+6 -2
View File
@@ -7,7 +7,7 @@ export default class Constants
public static API_URL: string = 'https://va.hydev.org/api';
/** Current version */
public static VERSION: string = '0.3.6.741';
public static VERSION: string = '0.4.2.912';
/** Minimum version that still supports the same cookies */
public static MIN_SUPPORTED_VERSION: string = '0.3.4.561';
@@ -53,5 +53,9 @@ export default class Constants
'#724e58',
'#4b565b'
]
}
};
// Terms (TODO: Actually get the terms dynamically
public static TERMS = [new Date('Sep 04 2019'), new Date('Nov 01 2019'), new Date('Feb 02 2020')];
public static CURRENT_TERM = 1;
}
+181
View File
@@ -0,0 +1,181 @@
import {Assignment} from '@/components/app/app';
import JsonUtils from '@/utils/json-utils';
import {FormatUtils} from '@/utils/format-utils';
import {CourseUtils} from '@/utils/course-utils';
import Navigation from '@/components/navigation/navigation';
import {GPAUtils} from '@/utils/gpa-utils';
export default class Course
{
id: number;
assignmentsId: number;
name: string;
teacherName: string;
status: string;
rawAssignments: Assignment[];
rawLetterGrade?: string;
rawNumericGrade?: number;
level: string;
scaleUp: number;
grading:
{
method: string
weightingMap: {[index: string]: number}
};
computed:
{
termAssignments: Assignment[][]
allYearGrade: number
};
/**
* Construct a course with a course json object
*
* @param courseJson Course json object
*/
constructor(courseJson: any)
{
this.id = courseJson.id;
this.assignmentsId = courseJson.assignmentsId;
this.name = FormatUtils.parseText(courseJson.name).trim();
this.teacherName = courseJson.teacherName;
this.status = courseJson.status;
this.rawLetterGrade = courseJson.letterGrade;
this.rawNumericGrade = courseJson.numericGrade;
// Other api issue
if (this.rawLetterGrade == '')
{
this.rawNumericGrade = undefined;
this.rawLetterGrade = undefined;
}
// Level and scaleUp
let level = CourseUtils.detectLevel(this.name);
if (level != undefined)
{
this.level = level.level;
this.scaleUp = level.scaleUp;
}
else this.level = 'Unknown';
this.grading = courseJson.grading;
}
/**
* Load in assignments data
*
* @param data Assignments data
*/
loadAssignments(data: any)
{
// Load assignments
// Parse json and filter it
this.rawAssignments = JsonUtils.filterAssignments(data);
// Sort by date (Latest is at 0)
this.rawAssignments.sort((a, b) => b.date.getTime() - a.date.getTime());
// Filter assignments into terms
let termAssignments: Assignment[][] = [[], [], [], []];
let currentTerm = 0;
// Loop through it by time order
this.rawAssignments.forEach(a => termAssignments[a.gradingPeriod].push(a));
// Set computed data
this.computed = {termAssignments: termAssignments, allYearGrade: -1};
}
/**
* Is graded or not
*/
get isGraded(): boolean
{
// Skip future or past courses
if (this.status != 'active') return false;
// Skip courses without levels TODO: Ask for user input
if (this.level == 'None' || this.level == 'Unknown' || this.scaleUp == -1) return false;
// Skip courses without graded assignments
if (this.assignments.filter(a => a.complete == 'Complete').length == 0) return false;
// Skip if there are no grading scale
// if (course.grading.method == 'NOT_GRADED') return;
// Is graded
return true;
}
/**
* Get assignments of the selected time
*/
get assignments(): Assignment[]
{
let timeCode = Navigation.instance.getSelectedGradingPeriod();
// All year
if (timeCode == -1)
{
return this.rawAssignments;
}
// Specific time
return this.computed.termAssignments[timeCode];
}
// TODO: Optimize this
private letterGradeComputed = false;
/**
* Get letter grade
*/
get letterGrade(): string
{
// Cached
if (this.rawLetterGrade != undefined && this.letterGradeComputed)
return this.rawLetterGrade;
this.letterGradeComputed = true;
// Get scale
let scale = GPAUtils.findScale(this.numericGrade);
// Scale not found
if (scale == undefined) return this.rawLetterGrade = '--';
// Return
return this.rawLetterGrade = scale.letter;
}
private numericGradeComputed = false;
/**
* Get numeric grade
*/
get numericGrade(): number
{
// Cached
if (this.rawNumericGrade != undefined && this.numericGradeComputed)
return this.rawNumericGrade;
this.numericGradeComputed = true;
// Calculate
if (this.grading.method == 'PERCENT_TYPE')
{
return this.rawNumericGrade = GPAUtils.getPercentTypeAverage(this, this.assignments);
}
if (this.grading.method == 'TOTAL_MEAN')
{
return this.rawNumericGrade = GPAUtils.getTotalMeanAverage(this.assignments);
}
// Error
return -1;
}
}
+2 -2
View File
@@ -1,10 +1,10 @@
import Vue from 'vue';
import ElementUI from 'element-ui';
const VCharts = require('v-charts');
import App from './components/app/app.vue';
import VueCookies from 'vue-cookies';
const VCharts = require('v-charts');
Vue.config.productionTip = false;
// Use Element UI
@@ -0,0 +1,80 @@
<template>
<div id="assignment-type-head">
<el-card :body-style="{padding: '0px'}">
<div id="type-info-card">
<span id="type-name">{{typeName}}</span>
<span id="type-average">Average: {{average.toFixed(2)}}%</span>
</div>
<AssignmentEntry v-for="assignment of filteredAssignments" :key="assignment.id"
:assignment="assignment" :unread="false"
backgroundColor="#ffffff" narrow="true">
</AssignmentEntry>
</el-card>
</div>
</template>
<script lang="ts">
import {Component, Prop, Vue} from 'vue-property-decorator';
import {Assignment} from '@/components/app/app';
import AssignmentEntry from '@/pages/overall/overall-course/assignment-entry/assignment-entry.vue';
@Component({
components: {AssignmentEntry}
})
export default class AssignmentTypeHead extends Vue
{
@Prop({required: true}) typeName: string;
@Prop({required: true}) assignments: Assignment[];
get filteredAssignments()
{
// Filter assignments to only this type
return this.assignments.filter(a => a.complete == 'Complete' && a.type == this.typeName);
}
get average()
{
return this.filteredAssignments.reduce((a, b) => a + b.score, 0) /
this.filteredAssignments.reduce((a, b) => a + b.scoreMax, 0) * 100;
}
}
</script>
<style lang="scss" scoped>
#type-info-card
{
height: 60px;
}
#type-name
{
// Font
font-size: 22px;
color: var(--main);
// Center
height: 60px;
line-height: 60px;
// Alignment
padding-left: 20px;
float: left;
}
#type-average
{
// Font
font-size: 14px;
color: #8db3e4;
// Center
height: 60px;
line-height: 64px;
// Alignment
float: left;
margin-left: 15px;
display: inline-block;
}
</style>
-29
View File
@@ -1,29 +0,0 @@
import {Component, Prop, Vue} from 'vue-property-decorator';
import {Assignment, Course} from '@/components/app/app';
import CourseHead from '@/pages/overall/overall-course/course-head/course-head.vue';
import CourseScatter from '@/pages/course/course-scatter/course-scatter';
@Component({
components: {CourseHead, CourseScatter}
})
export default class CoursePage extends Vue
{
// @ts-ignore
@Prop({required: true}) course: Course;
private unread: number = -1;
private unreadAssignments: Assignment[] = [];
/**
* Count the number of unread assignments with cache
*/
countUnread(): number
{
if (this.unread == -1)
{
this.unreadAssignments = this.course.assignments.filter(a => a.unread);
return this.unread = this.unreadAssignments.length;
}
else return this.unread;
}
}
+54 -2
View File
@@ -1,6 +1,6 @@
<template>
<el-card id="course-card" class="course-card">
<course-head :course="course" :unread="countUnread()"></course-head>
<course-head :clickable="false" :course="course" :unread="countUnread()"></course-head>
<div class="course-card-content expand">
<el-row>
@@ -13,9 +13,61 @@
</el-col>
</el-row>
<AssignmentTypeHead v-for="type in getAssignmentTypes()" :key="type"
:type-name="type" :assignments="course.assignments">
</AssignmentTypeHead>
</div>
</el-card>
</template>
<script src="./course-page.ts" lang="ts"></script>
<script lang="ts">
import {Component, Prop, Vue} from 'vue-property-decorator';
import {Assignment} from '@/components/app/app';
import CourseHead from '@/pages/overall/overall-course/course-head/course-head.vue';
import CourseScatter from '@/pages/course/course-scatter/course-scatter';
import AssignmentEntry from '@/pages/overall/overall-course/assignment-entry/assignment-entry.vue';
import AssignmentTypeHead from '@/pages/course/assignment-type-head/assignment-type-head.vue';
import Course from '@/logic/course';
@Component({
components: {AssignmentEntry, CourseHead, CourseScatter, AssignmentTypeHead}
})
export default class CoursePage extends Vue
{
@Prop({required: true}) course: Course;
private unread: number = -1;
private unreadAssignments: Assignment[] = [];
/**
* Count the number of unread assignments with cache
*/
countUnread(): number
{
if (this.unread == -1)
{
this.unreadAssignments = this.course.assignments.filter(a => a.unread);
return this.unread = this.unreadAssignments.length;
}
else return this.unread;
}
/**
* Get all the types of the assignments.
*/
getAssignmentTypes(): string[]
{
// Get all types
let types = this.course.assignments.map(a => a.type);
// Remove duplicates
types = types.filter((type, i, a) => a.indexOf(type) == i);
// Return it
return types;
}
}
</script>
<style src="./course-page.scss" lang="scss" scoped></style>
@@ -1,9 +1,9 @@
import {Component, Prop, Vue} from 'vue-property-decorator';
import {Assignment, Course} from '@/components/app/app';
import {GPAUtils} from '@/utils/gpa-utils';
import {Assignment} from '@/components/app/app';
import Constants from '@/constants';
import {FormatUtils} from '@/utils/format-utils';
import moment from 'moment';
import Course from '@/logic/course';
@Component({
})
@@ -11,7 +11,6 @@ export default class CourseScatter extends Vue
{
private static DOT = '<span style="display:inline-block;margin-right:5px;border-radius:10px;width:9px;height:9px;background-color:{color}"></span>';
// @ts-ignore
@Prop({required: true}) course: Course;
/**
+14 -7
View File
@@ -1,5 +1,5 @@
import {Component, Prop, Vue} from 'vue-property-decorator';
import {Course} from '@/components/app/app';
import Course from '@/logic/course';
import {GPAUtils} from '@/utils/gpa-utils';
import Constants from '@/constants';
import {FormatUtils} from '@/utils/format-utils';
@@ -8,7 +8,6 @@ import {FormatUtils} from '@/utils/format-utils';
})
export default class OverallBar extends Vue
{
// @ts-ignore
@Prop({required: true}) courses: Course[];
/**
@@ -54,15 +53,16 @@ export default class OverallBar extends Vue
// Data
series:
[
// Max GP
{
type: 'bar',
barGap: '-100%',
data: this.courses.map(course =>
{
return {value: [course.name, GPAUtils.getGP(course, 'A+')],
itemStyle: {color: '#d8d8d8'}}
return {value: [course.name, GPAUtils.getGP(course, 'A+')], itemStyle: {color: '#d8d8d8'}}
}),
},
// Current GP
{
type: 'bar',
barGap: '-100%',
@@ -93,14 +93,21 @@ export default class OverallBar extends Vue
{
let data: any = [];
this.courses.forEach(course =>
this.courses.forEach((course, index) =>
{
// Get GP
let gp = GPAUtils.getGP(course, course.letterGrade);
// No grade cases
if (gp == -1) return;
// Push data
data.push(
{
value: [course.name, GPAUtils.getGP(course, course.letterGrade)],
value: [course.name, gp],
itemStyle:
{
color: Constants.THEME.colors[data.length]
color: Constants.THEME.colors[index]
}
});
});
@@ -1,6 +1,6 @@
// Row
.unread-entry
.assignment-entry
{
height: 40px;
padding: 0 10px 0 20px;
@@ -16,6 +16,10 @@
span.month
{
margin-right: 5px;
// Unified width
display: inline-block;
min-width: 50px;
}
span.now
@@ -95,9 +99,40 @@
height: 22px;
padding: 0 5px;
}
// Unified width
.entry-box.score, .entry-box.max
{
min-width: 30px;
display: inline-block;
text-align: center;
}
// Unified width
.entry-box.percent
{
min-width: 60px;
display: inline-block;
text-align: right;
}
}
.unread-entry:first-child
// Narrow layout
.assignment-entry.narrow
{
height: 34px;
}
// Unread
.no-unread
{
visibility: hidden !important;
width: 0 !important;
margin-left: 0 !important;
padding: 0 0 0 10px !important;
}
.assignment-entry:first-child
{
padding-top: 3px;
@@ -0,0 +1,71 @@
<template>
<div class="assignment-entry vertical-center"
:class="narrow ? 'narrow' : ''"
:style="`background: ${backgroundColor}`">
<el-row class="unread-row">
<el-col :span="3" class="date">
<span class="month">{{getMoment(assignment.date).format("MMM D")}}</span>
<span class="now">({{getMoment(assignment.date).fromNow()}})</span>
</el-col>
<el-col :span="15" class="description">
<span class="type entry-box"
:style="`border-color: var(--assignment-type-${assignment.typeId})`">
{{assignment.type}}
</span>
<span class="text">{{assignment.description}}</span>
</el-col>
<el-col :span="6" class="grade">
<span class="percent entry-box">
{{(assignment.score / assignment.scoreMax * 100).toFixed(1)}}
<span class="symbol">%</span>
</span>
<span class="score entry-box">{{assignment.score}}</span>
<span class="max entry-box">{{assignment.scoreMax}}</span>
<el-button class="mark-as-read" :class="unread ? 'unread' : 'no-unread'"
size="mini" type="text" icon="el-icon-close"
@click="markAsRead">
</el-button>
</el-col>
</el-row>
</div>
</template>
<script lang="ts">
import {Component, Prop, Vue} from 'vue-property-decorator';
import {Assignment} from '@/components/app/app';
import moment from 'moment';
@Component
export default class AssignmentEntry extends Vue
{
@Prop({required: true}) assignment: Assignment;
@Prop({default: false}) unread: boolean;
@Prop({default: '#f5f7fa'}) backgroundColor: string;
@Prop({default: false}) narrow: boolean;
/**
* Format a date to the displayed format
*
* @param date Date
*/
getMoment(date: string)
{
return moment(new Date(date));
}
/**
* Mark this unread assignment as read
*/
markAsRead()
{
// Call custom event
this.$emit('mark-as-read', this.assignment)
}
}
</script>
<style src="./assignment-entry.scss" lang="scss" scoped></style>
@@ -0,0 +1,92 @@
// Main card content
.course-card-content.main
{
padding: 0 20px 0 20px;
height: 90px;
// Main color
background: white;
}
.course-col-name
{
// Align left
text-align: left;
float: left;
.course-name
{
overflow: hidden;
font-size: 22px;
color: var(--main);
}
.course-teacher
{
font-size: 12px;
color: #999999;
font-style: italic;
}
}
.course-col-grade
{
// Align right
text-align: right;
float: right;
// Adjust position
margin-top: -2px;
.course-grade
{
font-size: 21px;
}
.course-updates
{
font-size: 14px;
.unread-number
{
display: inline-block;
width: 20px;
text-align: center;
border-radius: 5px;
padding-left: 3px;
padding-right: 3px;
margin-right: 3px;
}
.unread-text
{
font-style: italic;
}
}
.course-updates.unread
{
.unread-number
{
background: var(--unread);
color: white;
}
.unread-text
{
color: var(--unread);
}
}
.course-updates.none
{
color: #999999;
.unread-number
{
background: #eeeeee;
}
}
}
@@ -9,8 +9,8 @@
<el-col :span="12" class="course-col-grade">
<div class="course-grade">
<span class="letter">{{course.letterGrade}} </span>
<span class="numeric">{{course.numericGrade.toFixed(2)}}</span>
<span class="percent">%</span>
<span class="numeric">{{course.numericGrade === undefined ? '--' : course.numericGrade.toFixed(2)}}</span>
<span class="percent" v-if="course.numericGrade !== undefined">%</span>
</div>
<div class="course-updates" @click="redirect" :class="unread === 0 ? 'none' : 'unread'">
<span class="unread-number">{{unread}}</span>
@@ -25,23 +25,18 @@
<script lang="ts">
import {Component, Prop, Vue} from 'vue-property-decorator';
import App, {Course} from '@/components/app/app';
import Course from '@/logic/course';
import {CourseUtils} from '@/utils/course-utils';
import Navigation from '@/components/navigation/navigation';
@Component({
components: {}
})
@Component
export default class CourseHead extends Vue
{
// @ts-ignore
@Prop() unread: number;
@Prop({required: true}) unread: number;
// @ts-ignore
@Prop() course: Course;
@Prop({required: true}) course: Course;
// @ts-ignore
@Prop() clickable: boolean;
@Prop({required: true}) clickable: boolean;
/**
* Redirect to the course page
@@ -54,97 +49,4 @@
}
</script>
<style lang="scss">
// Main card content
.course-card-content.main
{
padding: 0 20px 0 20px;
height: 90px;
// Main color
background: white;
}
.course-col-name
{
// Align left
text-align: left;
float: left;
.course-name
{
overflow: hidden;
font-size: 22px;
color: var(--main);
}
.course-teacher
{
font-size: 12px;
color: #999999;
font-style: italic;
}
}
.course-col-grade
{
// Align right
text-align: right;
float: right;
// Adjust position
margin-top: -2px;
.course-grade
{
font-size: 21px;
}
.course-updates
{
font-size: 14px;
.unread-number
{
display: inline-block;
width: 20px;
text-align: center;
border-radius: 5px;
padding-left: 3px;
padding-right: 3px;
margin-right: 3px;
}
.unread-text
{
font-style: italic;
}
}
.course-updates.unread
{
.unread-number
{
background: var(--unread);
color: white;
}
.unread-text
{
color: var(--unread);
}
}
.course-updates.none
{
color: #999999;
.unread-number
{
background: #eeeeee;
}
}
}
</style>
<style src="./course-head.scss" lang="scss"></style>
@@ -1,16 +1,14 @@
import {Component, Prop, Vue} from 'vue-property-decorator';
import App, {Assignment, Course} from '@/components/app/app';
import {GPAUtils} from '@/utils/gpa-utils';
import Constants from '@/constants';
import UnreadEntry from '@/pages/overall/overall-course/unread-entry/unread-entry';
import App, {Assignment} from '@/components/app/app';
import AssignmentEntry from '@/pages/overall/overall-course/assignment-entry/assignment-entry.vue';
import CourseHead from '@/pages/overall/overall-course/course-head/course-head.vue';
import Course from '@/logic/course';
@Component({
components: {UnreadEntry, CourseHead}
components: {UnreadEntry: AssignmentEntry, CourseHead}
})
export default class OverallCourse extends Vue
{
// @ts-ignore
@Prop({required: true}) course: Course;
private unread: number = -1;
@@ -7,6 +7,7 @@
<unread-entry v-for="assignment in unreadAssignments"
:assignment="assignment"
:key="assignment.id"
unread="true"
v-on:mark-as-read="markAsRead">
</unread-entry>
</div>
@@ -1,30 +0,0 @@
import {Component, Prop, Vue} from 'vue-property-decorator';
import {Assignment, Course} from '@/components/app/app';
import moment from 'moment';
@Component({
})
export default class UnreadEntry extends Vue
{
// @ts-ignore
@Prop({required: true}) assignment: Assignment;
/**
* Format a date to the displayed format
*
* @param date Date
*/
getMoment(date: string)
{
return moment(new Date(date));
}
/**
* Mark this unread assignment as read
*/
markAsRead()
{
// Call custom event
this.$emit('mark-as-read', this.assignment)
}
}
@@ -1,31 +0,0 @@
<template>
<div class="unread-entry vertical-center">
<el-row class="unread-row">
<el-col :span="3" class="date">
<span class="month">{{getMoment(assignment.date).format("MMM Do")}}</span>
<span class="now">({{getMoment(assignment.date).fromNow()}})</span>
</el-col>
<el-col :span="15" class="description">
<span class="type entry-box"
:style="`border-color: var(--assignment-type-${assignment.typeId})`">
{{assignment.type}}
</span>
<span class="text">{{assignment.description}}</span>
</el-col>
<el-col :span="6" class="grade">
<span class="percent entry-box">
{{(assignment.score / assignment.scoreMax * 100).toFixed(1)}}
<span class="symbol">%</span>
</span>
<span class="score entry-box">{{assignment.score}}</span>
<span class="max entry-box">{{assignment.scoreMax}}</span>
<el-button class="mark-as-read" size="mini" type="text"
icon="el-icon-close" @click="markAsRead">
</el-button>
</el-col>
</el-row>
</div>
</template>
<script src="./unread-entry.ts" lang="ts"></script>
<style src="./unread-entry.scss" lang="scss" scoped></style>
+60 -16
View File
@@ -1,11 +1,11 @@
import {Component, Prop, Vue} from 'vue-property-decorator';
import {Course} from '@/components/app/app';
import moment from 'moment';
import Course from '@/logic/course';
@Component({
})
export default class OverallLine extends Vue
{
// @ts-ignore
@Prop({required: true}) courses: Course[];
private settings =
@@ -26,9 +26,6 @@ export default class OverallLine extends Vue
legend:
{
show: false,
//left: 'auto',
//align: 'left',
//orient: 'vertical'
textStyle:
{
fontSize: 11
@@ -38,9 +35,8 @@ export default class OverallLine extends Vue
// Zoom bar
dataZoom:
[
// TODO: Calculate real value for startValue
{
startValue: '9/13/2019'
startValue: moment().subtract(30, 'days').format('M/D/YYYY')
},
{
type: 'inside'
@@ -48,7 +44,51 @@ export default class OverallLine extends Vue
],
series:
{
smooth: true
smooth: true,
// Mark area
markArea:
{
silent: true,
data:
[
// Above 100
[
{
yAxis: 120,
itemStyle: {color: 'rgba(230,253,255,0.09)'}
}, {yAxis: 100}
],
// 90 to 100
[
{
yAxis: 100,
itemStyle: {color: 'rgba(241,255,237,0.09)'}
}, {yAxis: 90}
],
// 80 to 90
[
{
yAxis: 90,
itemStyle: {color: 'rgba(255,250,216,0.09)'}
}, {yAxis: 80}
],
// 70 to 80
[
{
yAxis: 80,
itemStyle: {color: 'rgba(255,225,199,0.1)'}
}, {yAxis: 70}
],
// Below 70 (Fail)
[
{
yAxis: 70,
itemStyle: {color: 'rgb(255,190,184, 0.09)'}
}, {yAxis: -100}
]
]
}
},
xAxis:
{
@@ -57,23 +97,28 @@ export default class OverallLine extends Vue
yAxis:
{
min: (value: any) => Math.floor(value.min),
max: (value: any) => value.max
max: (value: any) => Math.min(value.max, 110)
}
};
chartCache: any;
/**
* Convert assignments list to a graph dataset.
*/
get convertChart()
{
let courses = this.courses;
// Caching
if (this.chartCache != undefined) return this.chartCache;
let courses = this.courses.filter(c => c.assignments.length > 0);
// Compute the column names
let columns = courses.map(course => course.name);
columns.unshift('date');
// Find the min date
let minDates = courses.map(course => new Date(course.assignments[course.assignments.length - 1].date).getTime());
let minDates = courses.map(course => course.assignments[course.assignments.length - 1].date.getTime());
let minDate: Date = new Date(Math.min.apply(null, minDates));
// Find the dates in between
@@ -107,8 +152,7 @@ export default class OverallLine extends Vue
if (assignment.complete != 'Complete') return;
// Date is being looked at
let assignmentDate = new Date(assignment.date);
if (assignmentDate.getTime() < date.getTime())
if (assignment.date.getTime() < date.getTime())
{
// Record scores
score += assignment.score;
@@ -131,8 +175,7 @@ export default class OverallLine extends Vue
if (assignment.complete != 'Complete') return;
// Date is being looked at
let assignmentDate = new Date(assignment.date);
if (assignmentDate.getTime() < date.getTime())
if (assignment.date.getTime() < date.getTime())
{
// Record scores
if (typeScores[assignment.type] == undefined) typeScores[assignment.type] = 0;
@@ -172,7 +215,8 @@ export default class OverallLine extends Vue
rows.push(row);
});
return {
return this.chartCache =
{
columns: columns,
rows: rows
}
+11
View File
@@ -34,6 +34,17 @@
font-size: 11px;
}
.no-grade
{
font-size: 30px;
color: #b1b1b1;
// Disable selecting
display:block;
pointer-events: none;
user-select: none;
}
// Cards
.el-card.overall-bar-card
{
-24
View File
@@ -1,24 +0,0 @@
import {Component, Prop, Vue} from 'vue-property-decorator';
import OverallLine from '@/pages/overall/overall-line/overall-line';
import OverallBar from '@/pages/overall/overall-bar/overall-bar';
import OverallCourse from '@/pages/overall/overall-course/overall-course';
import {Course} from '@/components/app/app';
import {GPAUtils} from '@/utils/gpa-utils';
@Component({
components: {OverallLine, OverallBar, OverallCourse}
})
export default class Overall extends Vue
{
// @ts-ignore
@Prop({required: true}) courses: Course[];
/**
* This function is called to get gpa since I can't import another
* class in the Vue file.
*/
public getGPA()
{
return GPAUtils.getGPA(this.courses);
}
}
+33 -2
View File
@@ -1,6 +1,6 @@
<template>
<div id="overall">
<el-row>
<el-row v-if="getGPA().gpa !== -1">
<el-col :span="4">
<el-card class="large gpa-card vertical-center">
<div style="padding: 14px;">
@@ -25,6 +25,12 @@
</el-col>
</el-row>
<el-row v-if="getGPA().gpa === -1">
<el-card class="large gpa-card vertical-center">
<div class="no-grade">This quarter has no grades yet...</div>
</el-card>
</el-row>
<overall-course v-for="course in courses"
:course="course"
:key="course.id">
@@ -32,5 +38,30 @@
</div>
</template>
<script src="./overall.ts" lang="ts"></script>
<script lang="ts">
import {Component, Prop, Vue} from 'vue-property-decorator';
import OverallLine from '@/pages/overall/overall-line/overall-line';
import OverallBar from '@/pages/overall/overall-bar/overall-bar';
import OverallCourse from '@/pages/overall/overall-course/overall-course';
import Course from '@/logic/course';
import {GPAUtils} from '@/utils/gpa-utils';
@Component({
components: {OverallLine, OverallBar, OverallCourse}
})
export default class Overall extends Vue
{
@Prop({required: true}) courses: Course[];
/**
* This function is called to get gpa since I can't import another
* class in the Vue file.
*/
public getGPA()
{
return GPAUtils.getGPA(this.courses);
}
}
</script>
<style src="./overall.scss" lang="scss" scoped></style>
+1 -1
View File
@@ -1,4 +1,4 @@
import Vue, { VNode } from 'vue';
import Vue, {VNode} from 'vue';
declare global {
namespace JSX {
+4 -3
View File
@@ -1,4 +1,5 @@
declare module '*.vue' {
import Vue from 'vue';
export default Vue;
declare module '*.vue'
{
import Vue from 'vue';
export default Vue;
}
+53 -34
View File
@@ -1,40 +1,23 @@
import {Course} from '@/components/app/app';
import Course from '@/logic/course';
const LEVEL_AP = {level: 'AP', scaleUp: 1};
const LEVEL_H = {level: 'H', scaleUp: 0.75};
const LEVEL_A = {level: 'A', scaleUp: 0.5};
const LEVEL_CP = {level: 'CP', scaleUp: 0.25};
const LEVEL_CLUB = {level: 'Club', scaleUp: -1};
const UNKNOWN_COURSE_LIST = new Map();
UNKNOWN_COURSE_LIST.set('Piano Masterclass', LEVEL_H);
UNKNOWN_COURSE_LIST.set('Multivariable Calculus with Differential Equations', LEVEL_H);
UNKNOWN_COURSE_LIST.set('Introduction to Algorithmic Thinking and Computational Technologies', LEVEL_A);
UNKNOWN_COURSE_LIST.set('Ceramics 1', LEVEL_CP);
UNKNOWN_COURSE_LIST.set('Ceramics 2', LEVEL_A);
UNKNOWN_COURSE_LIST.set('Sculpture', LEVEL_CP);
UNKNOWN_COURSE_LIST.set('Drawing', LEVEL_CP);
UNKNOWN_COURSE_LIST.set('Painting', LEVEL_CP);
export class CourseUtils
{
/**
* Return a list of courses that are graphed
*
* @param original Original course list
* @return Course[] Filtered course list
*/
public static getGradedCourses(original: Course[]): Course[]
{
// Define result
let result: Course[] = [];
// Filter through courses
original.forEach(course =>
{
// Skip future or past courses
if (course.status != 'active') return;
// Skip courses without levels
if (course.level == 'None') return;
// Skip courses without graded assignments
if (course.assignments.filter(a => a.complete == 'Complete').length == 0) return;
// Skip if there are no grading scale
// if (course.grading.method == 'NOT_GRADED') return;
// Add it to the list
result.push(course);
});
return result;
}
/**
* Format course to tab index string
*
@@ -45,4 +28,40 @@ export class CourseUtils
{
return `course/${course.id}/${course.name.toLowerCase().split(' ').join('-')}`;
}
/**
* Detect course level based on course name
*
* @param name Course name
*/
static detectLevel(name: string)
{
// Common ones
if (name.startsWith('AP')) return LEVEL_AP;
if (name.endsWith(' H')) return LEVEL_H;
if (name.endsWith(' A')) return LEVEL_A;
if (name.endsWith(' CP')) return LEVEL_CP;
if (name.startsWith('HS ')) return LEVEL_CLUB;
if (name.startsWith('MS ')) return LEVEL_CLUB;
// Uncommon ones
let lower = name.toLowerCase();
if (name.startsWith('Pre-AP')) return LEVEL_AP;
if (lower.endsWith(' acc')) return LEVEL_A;
if (name.endsWith('H')) return LEVEL_H;
if (name.endsWith('A')) return LEVEL_A;
if (name.endsWith('CP')) return LEVEL_CP;
// Even more uncommon
if (lower.includes('honors')) return LEVEL_H;
if (lower.includes('accelerated')) return LEVEL_A;
if (name.includes('Advanced')) return LEVEL_A;
// Unknown course list
if (UNKNOWN_COURSE_LIST.has(name)) return UNKNOWN_COURSE_LIST.get(name);
// Really unknown
return undefined;
}
}
+10
View File
@@ -37,4 +37,14 @@ export class FormatUtils
{
return str.replace(/\w\S*/g, s => s.charAt(0).toUpperCase() + s.substr(1).toLowerCase())
}
/**
* Parse html text
*
* @param str
*/
public static parseText(str: string): string
{
return str.replace(/&amp;/g, '&');
}
}
+99 -38
View File
@@ -1,31 +1,34 @@
import Course from '@/logic/course';
import {Assignment} from '@/components/app/app';
export interface Scale
{
min: number
letter: string
gp: number
}
/**
* This is an utility class to calculate GPA.
*/
import {Course} from '@/components/app/app';
export class GPAUtils
{
// [[Min score, Letter grade, Base GPA], ...]
public static SCALE =
public static SCALE: Scale[] =
[
[96.5, 'A+', 4.00],
[92.5, 'A' , 3.75],
[89.5, 'A-', 3.50],
[86.5, 'B+', 3.25],
[82.5, 'B' , 3.00],
[79.5, 'B-', 2.75],
[76.5, 'C+', 2.50],
[72.5, 'C' , 2.25],
[70.5, 'C-', 2.00],
[69.5, 'D' , 1.00],
[0 , 'F' , 0.00]
{min: 96.5, letter: 'A+', gp: 4.00},
{min: 92.5, letter: 'A' , gp: 3.75},
{min: 89.5, letter: 'A-', gp: 3.50},
{min: 86.5, letter: 'B+', gp: 3.25},
{min: 82.5, letter: 'B' , gp: 3.00},
{min: 79.5, letter: 'B-', gp: 2.75},
{min: 76.5, letter: 'C+', gp: 2.50},
{min: 72.5, letter: 'C' , gp: 2.25},
{min: 70.5, letter: 'C-', gp: 2.00},
{min: 69.5, letter: 'D' , gp: 1.00},
{min: 0 , letter: 'F' , gp: 0.00}
];
// Keywords
public static MIN = 0;
public static LETTER = 1;
public static GPA = 2;
/**
* Calculate GPA for a list of couses
*
@@ -81,40 +84,48 @@ export class GPAUtils
* @param course Course
* @param letterGrade Letter grade
*/
public static getGP(course: Course, letterGrade?: string): number
public static getGP(course: Course, letterGrade: string): number
{
// Find the GPA for this course.
for (let scale of this.SCALE)
// Get scale
let scale = this.findScale(letterGrade);
// No scale
if (scale == undefined) return -1;
// Add scaleUp if not failed.
return scale.gp == 0 ? 0 : scale.gp + course.scaleUp;
}
/**
* Find the scale for a grade
*
* @param grade Letter grade or numeric grade
*/
public static findScale(grade: string | number): Scale | undefined
{
// Letter grade
if (typeof grade == 'string')
{
// Letter grades are the same
if (scale[this.LETTER] == letterGrade)
{
// Get grade and add it
let grade = <number> scale[this.GPA];
// Add scaleUp if not failed.
if (grade != 0) grade += course.scaleUp;
// That's it
return grade;
}
return this.SCALE.find(scale => scale.letter == grade);
}
return -1;
// Numeric grade
return this.SCALE.find(scale => grade >= scale.min);
}
/**
* Calculate the total-mean (total/max) average
*
* @param course Course
* @param assignments
*/
public static getTotalMeanAverage(course: Course)
public static getTotalMeanAverage(assignments: Assignment[])
{
let score = 0;
let max = 0;
// Loop through assignments
course.assignments.forEach(assignment =>
assignments.forEach(assignment =>
{
// If assignment should be displayed
if (assignment.complete != 'Complete') return;
@@ -127,4 +138,54 @@ export class GPAUtils
// Return
return score / max * 100;
}
/**
* Calculate the percent type
* TODO: Combine it with overall-line
*
* @param course
* @param assignments
*/
public static getPercentTypeAverage(course: Course, assignments: Assignment[])
{
let typeScores: {[index: string]: any} = {};
let typeCounts: {[index: string]: any} = {};
// Loop through assignments
assignments.forEach(assignment =>
{
// If assignment should be displayed
if (assignment.complete != 'Complete') return;
// Record scores
if (typeScores[assignment.type] == undefined) typeScores[assignment.type] = 0;
typeScores[assignment.type] += assignment.score / assignment.scoreMax;
if (typeCounts[assignment.type] == undefined) typeCounts[assignment.type] = 0;
typeCounts[assignment.type] ++;
});
// Count total percentage (This is to avoid less than expected cases)
// Eg. If HW = 25% and Quiz = 75%, I have 1 hw and 0 quiz
// Without total percentage, the avg grade I get is 25%.
let totalPercentage = 0;
for (let type in course.grading.weightingMap)
{
if (typeScores[type] != undefined)
{
totalPercentage += course.grading.weightingMap[type];
}
}
// Count
let score = 0;
for (let type in typeScores)
{
let typeFactor = course.grading.weightingMap[type] / totalPercentage;
score += typeScores[type] * typeFactor / typeCounts[type];
}
// Add average to the row
return score * 100;
}
}
+7 -10
View File
@@ -10,18 +10,15 @@ export default class JsonUtils
*/
public static filterAssignments(assignments: any): Assignment[]
{
let result: Assignment[] = [];
assignments.assignments.forEach((assignment: any) =>
return assignments.assignments.map((assignment: any) =>
{
result.push(
{
return {
id: assignment.assignment_id,
scoreId: assignment.score_id,
type: assignment.assignment_type,
typeId: assignment.assignment_type_id,
description: assignment.assignment_description,
date: assignment._date,
date: new Date(assignment._date),
complete: assignment.completion_status,
include: assignment.include_in_calculated_grade == 1,
display: assignment.display_grade == 1,
@@ -29,10 +26,10 @@ export default class JsonUtils
unread: assignment.is_unread == 1,
scoreMax: assignment.maximum_score,
score: +assignment.raw_score
});
});
score: +assignment.raw_score,
return result;
gradingPeriod: +assignment.grading_period.replace('Quarter ', '') - 1
}
});
}
}
+4 -1
View File
@@ -24,7 +24,10 @@
"dom",
"dom.iterable",
"scripthost"
]
],
// Custom
"strictPropertyInitialization": false
},
"include": [
"src/**/*.ts",