Compare commits

...

81 Commits

Author SHA1 Message Date
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
Hykilpikonna 24befed17b [U] Pre-Release v0.3.6.741 2019-10-20 22:00:35 -04:00
Hykilpikonna 07991b2a0e [F] Update title after history 2019-10-20 21:59:11 -04:00
Hykilpikonna 51ea0c7a80 [+] Update title when updating history 2019-10-20 21:56:00 -04:00
Hykilpikonna 8b01428208 [+] Create function to find title 2019-10-20 21:55:39 -04:00
Hykilpikonna f69e2617d4 [+] Encapsulate toTitleCase() 2019-10-20 21:55:13 -04:00
Hykilpikonna ddfdb47b93 [+] Encapsulate findCourse 2019-10-20 21:49:51 -04:00
Hykilpikonna 14849f4211 [O] Move updateIndex logging to a better place 2019-10-20 21:42:44 -04:00
Hykilpikonna 067c599cb1 [F] Fix history pop state override issue 2019-10-20 21:42:18 -04:00
Hykilpikonna 33ceaa38c0 [O] Use Navigation.updateIndex instead 2019-10-20 21:39:08 -04:00
Hykilpikonna 0a0288c2ee [+] Make updateIndex public 2019-10-20 21:38:20 -04:00
Hykilpikonna 90f888bc4b [+] Add instance to navigation 2019-10-20 21:38:11 -04:00
Hykilpikonna 9bb34fb2a4 [O] Make course operations unselectable 2019-10-20 21:34:13 -04:00
Hykilpikonna 22b75a6b30 [+] Add .unselectable to global css 2019-10-20 21:33:43 -04:00
Hykilpikonna 6af8410698 [F] Fix logic mistake 2019-10-20 21:32:30 -04:00
Hykilpikonna f023c724fa [O] Don't update history for initial update 2019-10-20 21:31:22 -04:00
Hykilpikonna 6bbbe9cece [O] Encapsulate update history in updateIndex() 2019-10-20 21:31:05 -04:00
Hykilpikonna e6a4a04bb4 [+] Only show next course when next course isn't null 2019-10-20 21:27:42 -04:00
Hykilpikonna 5c4a391d96 [O] Add jsdocs for findNextCourse 2019-10-20 21:23:53 -04:00
Hykilpikonna a75b15d840 [O] Separate findNextCourse() and nextCourse() 2019-10-20 21:23:29 -04:00
Hykilpikonna 1322bd6326 [O] Adjust opacity 2019-10-20 21:21:47 -04:00
Hykilpikonna e2997c345c [F] Fix null case 2019-10-20 21:20:39 -04:00
Hykilpikonna 923a7e824f [O] Add drop shadows 2019-10-20 21:19:18 -04:00
Hykilpikonna 10ac0b5330 [O] Separate padding from common css 2019-10-20 21:16:10 -04:00
Hykilpikonna bfeba9da40 [O] Put prev-course under navbar 2019-10-20 21:15:56 -04:00
Hykilpikonna 752a865334 [O] Align prev-course upward 2019-10-20 21:14:36 -04:00
Hykilpikonna 5eda771070 [M] Move common css properties to common class 2019-10-20 21:13:51 -04:00
Hykilpikonna 0826080f82 [O] Add css class to both course operations 2019-10-20 21:13:15 -04:00
Hykilpikonna 6fab785a49 [+] Add previous course div 2019-10-20 21:11:03 -04:00
Hykilpikonna ecbcca5f14 [+] Add index offset to nextCourse() 2019-10-20 21:10:53 -04:00
Hykilpikonna 861de56f10 [O] Make cursor a pointer 2019-10-20 21:02:39 -04:00
Hykilpikonna 3ce66e1201 [+] Implement next course 2019-10-20 21:01:53 -04:00
Hykilpikonna 7e8ea73363 [O] Make it a trapezoid with rotateX 2019-10-20 20:56:51 -04:00
Hykilpikonna 9142525d21 [O] Adjust width 2019-10-20 20:56:13 -04:00
Hykilpikonna 62577ff1bb [O] Add padding 2019-10-20 20:56:06 -04:00
Hykilpikonna 953556ccee [O] Reduce font size 2019-10-20 20:51:47 -04:00
Hykilpikonna cc8621f304 [O] Add background 2019-10-20 20:51:41 -04:00
Hykilpikonna baae26d244 [O] Bottom align and center 2019-10-20 20:51:32 -04:00
Hykilpikonna cbf70cbeef [+] Create next course div in navbar 2019-10-20 20:48:18 -04:00
30 changed files with 518 additions and 272 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>
+12
View File
@@ -5,6 +5,7 @@
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-bottom: 100px;
}
#app-content
@@ -71,3 +72,14 @@ div.el-card.course-card > div.el-card__body
text-decoration: underline;
cursor: pointer;
}
// Non-selectable text
.unselectable
{
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
+6 -2
View File
@@ -1,7 +1,7 @@
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';
@@ -9,7 +9,8 @@ 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 {FormatUtils} from '@/utils/format-utils';
/**
@@ -146,6 +147,9 @@ export default class App extends Vue
// Save courses
this.courses = response.data;
// Post processing
CourseUtils.postProcess(this.courses);
// Load assignments
this.loadAssignments();
}
+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()
{
+16 -13
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,21 +22,26 @@ 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'))
{
// Check cookies version
if (this.needToUpdateCookies())
{
console.log('Version Updated! Clearing cookies...');
// Clear all cookies
this.$cookies.keys().forEach(key => this.$cookies.remove(key));
}
// 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 +75,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>
+36 -1
View File
@@ -46,5 +46,40 @@
// Font
font-size: 1.25rem;
display: inline-flex;
}
#next-course
{
// Down center
width: 50%;
position: absolute;
bottom: 0;
left: 25%;
padding-top: 2px;
box-shadow: 0 -2px 9px 0 #ecebeb;
}
#prev-course
{
// Up center
width: 50%;
position: absolute;
top: 61px;
left: 25%;
padding-bottom: 2px;
box-shadow: 0 2px 9px 0 #ecebeb;
}
.nav-course-operations
{
// Background
background-color: rgba(214, 214, 214, 0.67);
opacity: 0.85;
// Font
font-size: 14px;
color: #ab8585;
// Cursor
cursor: pointer;
}
+89 -21
View File
@@ -1,7 +1,8 @@
import {Component, Prop, Vue} from 'vue-property-decorator';
import Constants from '@/constants';
import {Course} from '@/components/app/app';
import App, {Course} from '@/components/app/app';
import {CourseUtils} from '@/utils/course-utils';
import {FormatUtils} from '@/utils/format-utils';
import pWaitFor from 'p-wait-for';
/**
* This component is the top navigation bar
@@ -11,23 +12,31 @@ import {CourseUtils} from '@/utils/course-utils';
})
export default class Navigation extends Vue
{
// @ts-ignore
@Prop() activeIndex: string;
@Prop({required: true}) activeIndex: string;
@Prop() courses: any;
@Prop({required: true}) courses: Course[];
// Instance
public static instance: Navigation;
/**
* This is called when the instance is created.
*/
public created()
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));
// 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 =>
@@ -36,7 +45,7 @@ export default class Navigation extends Vue
{
// Restore previous tab
console.log(`onPopState: Current: ${this.activeIndex}, Previous: ${e.state.lastTab}`);
this.updateIndex(e.state.lastTab);
this.updateIndex(e.state.lastTab, false);
}
};
}
@@ -56,25 +65,84 @@ export default class Navigation extends Vue
{
// Update active index
this.updateIndex(index);
// Debug output TODO: Remove this
console.log(`onNavigate: Previous: ${this.activeIndex}, New: ${index}`);
// Check url
let url = `/${index}`;
// Push history state
window.history.pushState({lastTab: index}, '', url);
}
/**
* Update index
*
* @param newIndex New index
* @param history Record in history or not (Default true)
*/
private updateIndex(newIndex: string)
public updateIndex(newIndex: string, history?: boolean)
{
// Call custom event
this.$emit('update:activeIndex', newIndex);
// Record or not
if (history == null || history)
{
// Check url
let url = `/#${newIndex}`;
// Push history state
window.history.pushState({lastTab: newIndex}, '', url);
}
// Update title
document.title = 'Veracross Analyzer - ' + this.getTitle(newIndex);
}
/**
* Get title for index
*
* @param index Index
*/
public getTitle(index: string)
{
// Course
if (index.startsWith('course'))
{
return this.findCourse(index.split('/')[1], 0).name;
}
// Others
return FormatUtils.toTitleCase(index);
}
/**
* Move to the next course
*
* @param indexOffset Index offset (Eg. 1 for next)
*/
public nextCourse(indexOffset: number)
{
// Set tab to the next index
this.updateIndex(CourseUtils.formatTabIndex(this.findNextCourse(indexOffset)))
}
/**
* Find the next course
*
* @param indexOffset Index offset (Eg. 1 for next)
*/
public findNextCourse(indexOffset: number)
{
return this.findCourse(this.activeIndex.split('/')[1], indexOffset);
}
/**
* Find course
*
* @param courseId Course ID
* @param indexOffset Index offset (Eg. 1 for next)
*/
public findCourse(courseId: string, indexOffset: number)
{
// Find current course index
let courseIndex = this.courses.findIndex(c => c.id == +courseId);
// Find next course
return this.courses[courseIndex + indexOffset];
}
/**
+8 -1
View File
@@ -18,7 +18,14 @@
<el-button @click="signOut" id="sign-out-button" type="text">Sign Out</el-button>
</el-menu>
<div class="line"></div>
<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>
</div>
</template>
+1 -1
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.5.703';
public static VERSION: string = '0.3.6.784';
/** Minimum version that still supports the same cookies */
public static MIN_SUPPORTED_VERSION: string = '0.3.4.561';
+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
-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;
}
}
+37 -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,44 @@
</el-col>
</el-row>
<!--AssignmentEntry v-for="assignment in course.assignments"
:assignment="assignment" :unread="false">
</AssignmentEntry-->
</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, 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';
import AssignmentEntry from '@/pages/overall/overall-course/assignment-entry/assignment-entry.vue';
@Component({
components: {AssignmentEntry, CourseHead, CourseScatter}
})
export default class CoursePage extends Vue
{
@Prop({required: true}) course: Course;
private unread: number = -1;
private unreadAssignments: Assignment[] = [];
/**
* Count the number of unread assignments with cache
*/
countUnread(): number
{
if (this.unread == -1)
{
this.unreadAssignments = this.course.assignments.filter(a => a.unread);
return this.unread = this.unreadAssignments.length;
}
else return this.unread;
}
}
</script>
<style src="./course-page.scss" lang="scss" scoped></style>
@@ -1,6 +1,5 @@
import {Component, Prop, Vue} from 'vue-property-decorator';
import {Assignment, Course} from '@/components/app/app';
import {GPAUtils} from '@/utils/gpa-utils';
import Constants from '@/constants';
import {FormatUtils} from '@/utils/format-utils';
import moment from 'moment';
@@ -11,7 +10,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;
/**
@@ -8,7 +8,6 @@ import {FormatUtils} from '@/utils/format-utils';
})
export default class OverallBar extends Vue
{
// @ts-ignore
@Prop({required: true}) courses: Course[];
/**
@@ -5,6 +5,7 @@
<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})`">
@@ -12,6 +13,7 @@
</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)}}
@@ -19,13 +21,45 @@
</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">
icon="el-icon-close" @click="markAsRead" v-if="unread">
</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>
<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;
/**
* 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;
}
}
}
@@ -25,22 +25,18 @@
<script lang="ts">
import {Component, Prop, Vue} from 'vue-property-decorator';
import App, {Course} from '@/components/app/app';
import {Course} from '@/components/app/app';
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
@@ -48,102 +44,9 @@
redirect()
{
if (!this.clickable) return;
App.instance.selectedTab = CourseUtils.formatTabIndex(this.course);
Navigation.instance.updateIndex(CourseUtils.formatTabIndex(this.course));
}
}
</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,13 @@
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 AssignmentEntry from '@/pages/overall/overall-course/assignment-entry/assignment-entry.vue';
import CourseHead from '@/pages/overall/overall-course/course-head/course-head.vue';
@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,11 +1,11 @@
import {Component, Prop, Vue} from 'vue-property-decorator';
import {Course} from '@/components/app/app';
import moment from 'moment';
@Component({
})
export default class OverallLine extends Vue
{
// @ts-ignore
@Prop({required: true}) courses: Course[];
private settings =
@@ -38,9 +38,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'
-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);
}
}
+26 -1
View File
@@ -32,5 +32,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 '@/components/app/app';
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;
}
+81 -2
View File
@@ -1,4 +1,21 @@
import {Course} from '@/components/app/app';
import {FormatUtils} from '@/utils/format-utils';
const LEVEL_AP = {level: 'AP', scaleUp: 1};
const LEVEL_H = {level: 'H', scaleUp: 0.75};
const LEVEL_A = {level: 'A', scaleUp: 0.5};
const LEVEL_CP = {level: 'CP', scaleUp: 0.25};
const LEVEL_CLUB = {level: 'Club', scaleUp: -1};
const 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
{
@@ -19,8 +36,8 @@ export class CourseUtils
// Skip future or past courses
if (course.status != 'active') return;
// Skip courses without levels
if (course.level == 'None') return;
// Skip courses without levels TODO: Ask for user input
if (course.level == 'None' || course.level == 'Unknown' || course.scaleUp == -1) return;
// Skip courses without graded assignments
if (course.assignments.filter(a => a.complete == 'Complete').length == 0) return;
@@ -45,4 +62,66 @@ export class CourseUtils
{
return `course/${course.id}/${course.name.toLowerCase().split(' ').join('-')}`;
}
/**
* Post process course list
*
* @param courses Course list
*/
public static postProcess(courses: Course[])
{
for (let course of courses)
{
// Parse name
course.name = FormatUtils.parseText(course.name).trim();
// Detect level
let level = this.detectLevel(course.name);
if (level != undefined)
{
course.level = level.level;
course.scaleUp = level.scaleUp;
}
else
{
course.level = 'Unknown';
}
}
}
/**
* Detect course level based on course name
*
* @param name Course name
*/
private 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;
}
}
+22 -1
View File
@@ -22,8 +22,29 @@ export class FormatUtils
* @param str String
* @param length Max length
*/
static limit(str: string, length: number): string
public static limit(str: string, length: number): string
{
return str.length <= length ? str : str.substr(0, length - 2) + '...'
}
/**
* To Title Case
*
* @param str oRigInAL sTrING
* @return string Original String
*/
public static toTitleCase(str: string)
{
return str.replace(/\w\S*/g, s => s.charAt(0).toUpperCase() + s.substr(1).toLowerCase())
}
/**
* Parse html text
*
* @param str
*/
public static parseText(str: string): string
{
return str.replace(/&amp;/g, '&');
}
}
+4 -1
View File
@@ -24,7 +24,10 @@
"dom",
"dom.iterable",
"scripthost"
]
],
// Custom
"strictPropertyInitialization": false
},
"include": [
"src/**/*.ts",