Compare commits
146 Commits
0.5.1.1229
...
0.5.3.1373
| Author | SHA1 | Date | |
|---|---|---|---|
| 46ef56efbd | |||
| 7b04d1e006 | |||
| 511d4544b6 | |||
| 140893a9b2 | |||
| 427773f4cb | |||
| dfeb5d0272 | |||
| 304ba63771 | |||
| 1698f1737d | |||
| 9d8a797c54 | |||
| 4d0c48619d | |||
| e95d212628 | |||
| 936e6ffa41 | |||
| f2a3260b8f | |||
| 5c8b2b37d4 | |||
| 44e4b615ba | |||
| 60a852815b | |||
| 7c7d7e609a | |||
| 0da5be9448 | |||
| 1ebb69c89c | |||
| 99af1c4607 | |||
| e1caacce34 | |||
| e2b6a92743 | |||
| 982c8cf872 | |||
| 1e928ac1a8 | |||
| a17ab1511d | |||
| 07c55df766 | |||
| 06ee7b4de9 | |||
| 0c520d5f50 | |||
| 522867a2dc | |||
| d56d985734 | |||
| 1d404b4f51 | |||
| c1d3cc88bc | |||
| 1242e2ed58 | |||
| f61dccb90a | |||
| 106da7e7f1 | |||
| 4774ad658b | |||
| 6035fe5318 | |||
| 452ac69fb6 | |||
| 3ec604f049 | |||
| f5c093960a | |||
| 984fa394a5 | |||
| db12a5b9e7 | |||
| dc57ee16c9 | |||
| 975542dab2 | |||
| 5ad2be88c1 | |||
| 76ec5d4476 | |||
| 62237d5f5f | |||
| eeb2ae56d7 | |||
| bb1751f442 | |||
| a4241f9549 | |||
| 693d479b0f | |||
| cf5c59fb78 | |||
| 311055a4c9 | |||
| 02d5e209d9 | |||
| e4ebaf2935 | |||
| 97fe8395e6 | |||
| d7b319b6c9 | |||
| 1fdf6a8a72 | |||
| 3425894c24 | |||
| 3e6caab023 | |||
| 7e23919c0d | |||
| 6b7a5af7ae | |||
| 74bfec92f6 | |||
| 30030a0ca5 | |||
| 78590f96f1 | |||
| cc485708c5 | |||
| daeca0fb0b | |||
| 5b5704bd75 | |||
| 762be8bfba | |||
| b224f07e00 | |||
| b1a902f92e | |||
| aeadb6fc8b | |||
| e8aeecb27c | |||
| 29131227b5 | |||
| 9b3a61e5cc | |||
| 4bc9113b96 | |||
| 45e0ac6066 | |||
| 4047a3e8f6 | |||
| d4d8312016 | |||
| e60df664c1 | |||
| 916c586cbc | |||
| a903b2cdfa | |||
| 79f3a8e497 | |||
| 5905eb1923 | |||
| 6dd3d898aa | |||
| 7c86ecf3b5 | |||
| 45e1376a8f | |||
| 07f3544ba5 | |||
| 4f93539673 | |||
| 095dfdb54f | |||
| eaac2b9332 | |||
| e63547140b | |||
| 99aa7b4093 | |||
| b4e6dcdb8c | |||
| 2d182c3cf4 | |||
| 05feb8c9f3 | |||
| 3adfdb5aac | |||
| c0ec2e03ff | |||
| e128704552 | |||
| af2aba9a50 | |||
| dbdd33d050 | |||
| d230636a0e | |||
| e7d639af33 | |||
| b97ca4f699 | |||
| 1657330c57 | |||
| dd8bbf2cbe | |||
| 53065c9dd3 | |||
| 27bcd6b9b6 | |||
| bc190cbdfb | |||
| 755b384b76 | |||
| a3f03f5577 | |||
| 57f29262e3 | |||
| ac5549ccc4 | |||
| d3ba0af4f5 | |||
| 6312515b7b | |||
| 41cdbe93b1 | |||
| 702eb4d8f3 | |||
| 7581ea016e | |||
| 4938b2a72a | |||
| 815258e8be | |||
| adb211c58a | |||
| cdeae1e1d4 | |||
| dfbe255191 | |||
| f8f1c1f8a3 | |||
| 4e5809a1dc | |||
| aef5fa47fc | |||
| ca751c27d1 | |||
| 95690fe046 | |||
| 1fd17706e4 | |||
| 082400abe8 | |||
| e90741b6bc | |||
| 71352ee39a | |||
| caa6b38673 | |||
| 49c23c9562 | |||
| 94424ea288 | |||
| 956119be2a | |||
| e26decd77d | |||
| 4d7b41bf2a | |||
| e6cfa30ed4 | |||
| ea46d16836 | |||
| 128dbb2ca7 | |||
| 097985f087 | |||
| 26fa50ead2 | |||
| 14a9c5a9b2 | |||
| 55432e5c33 | |||
| a2f6a30ad1 |
@@ -1,5 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
#echo "Switch to production database settings"
|
||||
#exit
|
||||
|
||||
# abort on errors
|
||||
set -e
|
||||
|
||||
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 51 KiB |
@@ -11,9 +11,9 @@
|
||||
"@types/chroma-js": "^1.4.3",
|
||||
"@types/md5": "^2.1.33",
|
||||
"chroma-js": "^2.1.0",
|
||||
"core-js": "^2.6.5",
|
||||
"echarts": "^4.2.1",
|
||||
"element-ui": "^2.11.1",
|
||||
"core-js": "^2.6.10",
|
||||
"echarts": "^4.5.0",
|
||||
"element-ui": "^2.13.0",
|
||||
"md5": "^2.2.1",
|
||||
"moment": "^2.24.0",
|
||||
"p-wait-for": "^3.1.0",
|
||||
@@ -21,15 +21,15 @@
|
||||
"vue": "^2.6.10",
|
||||
"vue-class-component": "^7.0.2",
|
||||
"vue-cookies": "^1.5.13",
|
||||
"vue-property-decorator": "^8.1.0"
|
||||
"vue-property-decorator": "^8.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "^3.10.0",
|
||||
"@vue/cli-plugin-typescript": "^3.10.0",
|
||||
"@vue/cli-service": "^3.10.0",
|
||||
"node-sass": "^4.9.0",
|
||||
"sass-loader": "^7.1.0",
|
||||
"typescript": "^3.4.3",
|
||||
"@vue/cli-plugin-babel": "^3.12.1",
|
||||
"@vue/cli-plugin-typescript": "^3.12.1",
|
||||
"@vue/cli-service": "^4.1.1",
|
||||
"node-sass": "^4.13.0",
|
||||
"sass-loader": "^7.3.1",
|
||||
"typescript": "^3.7.3",
|
||||
"vue-template-compiler": "^2.6.10"
|
||||
},
|
||||
"postcss": {
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
|
||||
<!-- ElementUI -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
|
||||
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
|
||||
|
||||
<!-- Fonts -->
|
||||
<link href="https://fonts.googleapis.com/css?family=Nunito+Sans&display=swap" rel="stylesheet">
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Redirecting...</title>
|
||||
<meta http-equiv = "refresh" content = "0; url = https://vera.hydev.org/#info" />
|
||||
</head>
|
||||
<body>
|
||||
Redirecting to (<a href="https://vera.hydev.org/#info">https://vera.hydev.org/#info</a>)...
|
||||
<script>
|
||||
window.location.href = 'https://vera.hydev.org/#info';
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 44 KiB |
@@ -57,16 +57,17 @@
|
||||
}
|
||||
|
||||
// Overall
|
||||
#overall, #overall-course, .overall-span, #app-content
|
||||
#app-inner, #overall, #overall-course, .overall-span, #app-content
|
||||
{
|
||||
background: var(--dark-layer-1) !important;
|
||||
}
|
||||
|
||||
// Course card
|
||||
.entry-box, .none .unread-number {background: #a1a1a1 !important}
|
||||
.entry-box, .none .unread-number {background: #797979 !important}
|
||||
.entry-box.max {background-color: #949494 !important}
|
||||
.entry-box.percent {background-color: #a7a490 !important}
|
||||
.course-name {color: #cffff6 !important}
|
||||
#block-grade #updates.none #unread-number {background: #757575 !important}
|
||||
|
||||
.course-card-content.expand, .assignment-entry, .unread-row,
|
||||
.unread-row .el-col, #assignment-type-head, .course-page-graph.el-col
|
||||
@@ -74,14 +75,8 @@
|
||||
background-color: var(--dark-layer-3) !important;
|
||||
}
|
||||
|
||||
.overall-span.el-col, .course-page-graph.el-col
|
||||
{
|
||||
div, span
|
||||
{
|
||||
background: #f9f9f9 !important;
|
||||
color: var(--dark-layer-1) !important;
|
||||
}
|
||||
}
|
||||
// Nav bar
|
||||
.el-menu--horizontal>.el-menu-item.is-active {color: var(--dark-foreground) !important;}
|
||||
}
|
||||
|
||||
// ##############
|
||||
@@ -144,3 +139,9 @@ div.el-card.course-card > div.el-card__body
|
||||
{
|
||||
font-family: Nunito Sans, Helvetica Neue, Microsoft YaHei, "微软雅黑", Arial, sans-serif;
|
||||
}
|
||||
|
||||
// Fix word breaking
|
||||
.el-dialog__body
|
||||
{
|
||||
word-break: unset !important;
|
||||
}
|
||||
|
||||
@@ -9,53 +9,67 @@ import Loading from '@/components/overlays/loading.vue';
|
||||
import CoursePage from '@/pages/course/course-page.vue';
|
||||
import Course from '@/logic/course';
|
||||
import LoginUser from '@/logic/login-user';
|
||||
|
||||
import NavController from '@/logic/nav-controller';
|
||||
import Info from '@/statics/Info.vue';
|
||||
|
||||
@Component({
|
||||
components: {Login, Navigation, Overall, Loading, CoursePage},
|
||||
components: {Login, Navigation, Overall, Loading, CoursePage, Info},
|
||||
})
|
||||
export default class App extends Vue
|
||||
{
|
||||
// Is the login panel shown
|
||||
public showLogin: boolean = true;
|
||||
showLogin: boolean = true;
|
||||
|
||||
// List of course that the student takes
|
||||
public courses: Course[] = [];
|
||||
|
||||
// List of course that should be displayed
|
||||
public filteredCourses: Course[] = [];
|
||||
|
||||
// The currently selected tab
|
||||
public selectedTab: string = 'overall';
|
||||
courses: Course[] = [];
|
||||
gradedCourses: Course[] = [];
|
||||
|
||||
// Are the course assignments loaded from the server.
|
||||
public assignmentsReady: boolean = false;
|
||||
assignmentsReady: boolean = false;
|
||||
|
||||
// Token
|
||||
public user: LoginUser = null as any;
|
||||
user: LoginUser = null as any;
|
||||
|
||||
// Loading text
|
||||
public loading: string = '';
|
||||
loading: string = '';
|
||||
|
||||
// Loading error
|
||||
public loadingError: boolean = false;
|
||||
loadingError: boolean = false;
|
||||
|
||||
// Navigation controller
|
||||
nav: NavController = new NavController();
|
||||
|
||||
// Http Client
|
||||
public static http: HttpUtils = new HttpUtils();
|
||||
static http: HttpUtils = new HttpUtils();
|
||||
|
||||
// Instance
|
||||
public static instance: App;
|
||||
static instance: App;
|
||||
|
||||
// Static page
|
||||
staticPage: string = '';
|
||||
|
||||
// Dark mode
|
||||
darkMode: boolean = false;
|
||||
|
||||
/**
|
||||
* This is called when the instance is created.
|
||||
*/
|
||||
public created()
|
||||
created()
|
||||
{
|
||||
// Show splash
|
||||
console.log(Constants.SPLASH);
|
||||
|
||||
// Update instance
|
||||
App.instance = this;
|
||||
|
||||
// Check location
|
||||
if (window.location.hash == '#info')
|
||||
{
|
||||
this.staticPage = 'info';
|
||||
}
|
||||
|
||||
// Dark mode
|
||||
this.darkMode = this.$cookies.isKey('dark');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,7 +77,7 @@ export default class App extends Vue
|
||||
*
|
||||
* @param user Authorization user
|
||||
*/
|
||||
public onLogin(user: LoginUser)
|
||||
onLogin(user: LoginUser)
|
||||
{
|
||||
// Hide login bar
|
||||
this.showLogin = false;
|
||||
@@ -84,7 +98,7 @@ export default class App extends Vue
|
||||
/**
|
||||
* Load courses data after logging in.
|
||||
*/
|
||||
public loadCoursesAfterLogin()
|
||||
loadCoursesAfterLogin()
|
||||
{
|
||||
// Show loading message
|
||||
this.logLoading('2. Loading courses...');
|
||||
@@ -109,7 +123,7 @@ export default class App extends Vue
|
||||
/**
|
||||
* Load the assignments of the courses
|
||||
*/
|
||||
public loadAssignments()
|
||||
loadAssignments()
|
||||
{
|
||||
// Show loading message
|
||||
this.logLoading('3. Loading assignments...');
|
||||
@@ -134,7 +148,7 @@ export default class App extends Vue
|
||||
pWaitFor(() => this.courses.every(c => c.rawAssignments != null)).then(() =>
|
||||
{
|
||||
// Filter courses
|
||||
this.filteredCourses = this.courses.filter(c => c.isGraded);
|
||||
this.gradedCourses = this.courses.filter(c => c.isGraded);
|
||||
|
||||
// Check grading algorithms
|
||||
this.checkGradingAlgorithms();
|
||||
@@ -144,13 +158,13 @@ export default class App extends Vue
|
||||
/**
|
||||
* Check the courses' grading algorithms. (Total-mean or percent-type)
|
||||
*/
|
||||
private checkGradingAlgorithms()
|
||||
checkGradingAlgorithms()
|
||||
{
|
||||
// Show loading message
|
||||
this.logLoading('4. Checking grading algorithms...');
|
||||
|
||||
// Loop through all the courses
|
||||
for (const course of this.filteredCourses)
|
||||
for (const course of this.gradedCourses)
|
||||
{
|
||||
for (const i of [0, 1, 2, 3])
|
||||
{
|
||||
@@ -186,9 +200,8 @@ export default class App extends Vue
|
||||
}
|
||||
|
||||
// Wait for done
|
||||
pWaitFor(() => this.filteredCourses.every(c => c.termGrading.every(g => g != null))).then(() =>
|
||||
pWaitFor(() => this.gradedCourses.every(c => c.termGrading.every(g => g != null))).then(() =>
|
||||
{
|
||||
// When the assignments are ready
|
||||
this.assignmentsReady = true;
|
||||
|
||||
// Remove loading
|
||||
@@ -201,7 +214,7 @@ export default class App extends Vue
|
||||
*
|
||||
* @param message Message
|
||||
*/
|
||||
private logLoading(message: string)
|
||||
logLoading(message: string)
|
||||
{
|
||||
if (message == '') this.loading = '';
|
||||
else this.loading += '\n' + message;
|
||||
@@ -212,7 +225,7 @@ export default class App extends Vue
|
||||
*
|
||||
* @param message Error message
|
||||
*/
|
||||
private showError(message: string)
|
||||
showError(message: string)
|
||||
{
|
||||
this.loadingError = true;
|
||||
this.loading = message;
|
||||
@@ -221,7 +234,7 @@ export default class App extends Vue
|
||||
/**
|
||||
* Sign out
|
||||
*/
|
||||
public signOut()
|
||||
signOut()
|
||||
{
|
||||
// Clear all cookies
|
||||
this.$cookies.keys().forEach(key => this.$cookies.remove(key));
|
||||
@@ -235,7 +248,7 @@ export default class App extends Vue
|
||||
*
|
||||
* @param code
|
||||
*/
|
||||
public selectTime(code: number)
|
||||
selectTime(code: number)
|
||||
{
|
||||
// TODO: Optimize
|
||||
window.location.reload();
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
<template>
|
||||
<div id="app" class="theme-default">
|
||||
<login v-if="showLogin" v-on:login:user="onLogin"/>
|
||||
<navigation v-if="user != null"
|
||||
:courses="filteredCourses"
|
||||
:activeIndex.sync="selectedTab"
|
||||
:user="user"
|
||||
@sign-out="signOut" @select-time="selectTime">
|
||||
</navigation>
|
||||
<div id="app-inner" v-if="staticPage === ''" :class="{dark: darkMode}">
|
||||
<login v-if="showLogin" v-on:login:user="onLogin"/>
|
||||
<navigation v-if="user != null"
|
||||
:courses="gradedCourses"
|
||||
:user="user"
|
||||
:nav="nav"
|
||||
@sign-out="signOut" @select-time="selectTime">
|
||||
</navigation>
|
||||
|
||||
<div id="app-content" v-if="assignmentsReady && loading === ''">
|
||||
<overall v-if="selectedTab === 'overall'"
|
||||
:courses="filteredCourses">
|
||||
</overall>
|
||||
<course-page v-if="selectedTab.split('/')[0] === 'course'"
|
||||
:course="filteredCourses.find(c => +c.id === +selectedTab.split('/')[1])">
|
||||
</course-page>
|
||||
<div id="app-content" v-if="assignmentsReady && loading === ''">
|
||||
<overall v-if="nav.id === 'overall'"
|
||||
:courses="gradedCourses">
|
||||
</overall>
|
||||
<course-page v-if="nav.id === 'course'"
|
||||
:course="gradedCourses.find(c => +c.id === +nav.info.id)">
|
||||
</course-page>
|
||||
</div>
|
||||
|
||||
<loading v-if="loading !== ''" :text="loading" :error="loadingError"/>
|
||||
</div>
|
||||
|
||||
<loading v-if="loading !== ''" :text="loading" :error="loadingError"/>
|
||||
<Info v-if="staticPage === 'info'"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import App from '@/components/app/app';
|
||||
import {CourseUtils} from '@/logic/utils/course-utils';
|
||||
import {FormatUtils} from '@/logic/utils/format-utils';
|
||||
import pWaitFor from 'p-wait-for';
|
||||
import Course from '@/logic/course';
|
||||
import Constants from '@/constants';
|
||||
import LoginUser from '@/logic/login-user';
|
||||
import NavController from '@/logic/nav-controller';
|
||||
import App from '@/components/app/app';
|
||||
|
||||
/**
|
||||
* This component is the top navigation bar
|
||||
@@ -13,7 +11,7 @@ import LoginUser from '@/logic/login-user';
|
||||
@Component
|
||||
export default class Navigation extends Vue
|
||||
{
|
||||
@Prop({required: true}) activeIndex: string;
|
||||
@Prop({required: true}) nav: NavController;
|
||||
@Prop({required: true}) courses: Course[];
|
||||
@Prop({required: true}) user: LoginUser;
|
||||
|
||||
@@ -40,35 +38,7 @@ export default class Navigation extends Vue
|
||||
*/
|
||||
mounted()
|
||||
{
|
||||
// Set instance
|
||||
Navigation.instance = this;
|
||||
|
||||
// Set history state
|
||||
let url = '/' + window.location.hash;
|
||||
if (url == '/' || url == '') url = '/#overall';
|
||||
window.history.replaceState({lastTab: url.substring(1)}, '', url);
|
||||
|
||||
// Update initial index after loading is done
|
||||
pWaitFor(() => this.courses.length > 1 && App.instance.loading != '').then(() =>
|
||||
{
|
||||
this.updateIndex(url.substring(2), false);
|
||||
});
|
||||
|
||||
// Create history state listener
|
||||
window.onpopstate = e =>
|
||||
{
|
||||
if (e.state)
|
||||
{
|
||||
// Restore previous tab
|
||||
console.log(`onPopState: Current: ${this.activeIndex}, Previous: ${e.state.lastTab}`);
|
||||
this.updateIndex(e.state.lastTab, false);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
formatCourseIndex(course: Course)
|
||||
{
|
||||
return CourseUtils.formatTabIndex(course);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -80,52 +50,16 @@ export default class Navigation extends Vue
|
||||
onSelect(index: string, indexPath: string)
|
||||
{
|
||||
// Update active index
|
||||
this.updateIndex(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update index
|
||||
*
|
||||
* @param newIndex New index
|
||||
* @param history Record in history or not (Default true)
|
||||
*/
|
||||
updateIndex(newIndex: string, history?: boolean)
|
||||
{
|
||||
// Call custom event
|
||||
this.$emit('update:activeIndex', newIndex);
|
||||
|
||||
// Record or not
|
||||
if (history == null || history)
|
||||
try
|
||||
{
|
||||
// Check url
|
||||
let url = `/#${newIndex}`;
|
||||
|
||||
// Push history state
|
||||
window.history.pushState({lastTab: newIndex}, '', url);
|
||||
// Is json
|
||||
this.nav.updateIndex(JSON.parse(index))
|
||||
}
|
||||
|
||||
// Update title
|
||||
document.title = 'Veracross Analyzer - ' + this.getTitle(newIndex);
|
||||
|
||||
// Scroll to top
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get title for index
|
||||
*
|
||||
* @param index Index
|
||||
*/
|
||||
getTitle(index: string)
|
||||
{
|
||||
// Course
|
||||
if (index.startsWith('course'))
|
||||
catch (e)
|
||||
{
|
||||
return this.findCourse(index.split('/')[1], 0).name;
|
||||
// Not json
|
||||
this.nav.updateIndex(index);
|
||||
}
|
||||
|
||||
// Others
|
||||
return FormatUtils.toTitleCase(index);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -136,7 +70,7 @@ export default class Navigation extends Vue
|
||||
nextCourse(indexOffset: number)
|
||||
{
|
||||
// Set tab to the next index
|
||||
this.updateIndex(CourseUtils.formatTabIndex(this.findNextCourse(indexOffset)))
|
||||
this.nav.updateIndex(this.findNextCourse(indexOffset).urlIndex)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -146,7 +80,7 @@ export default class Navigation extends Vue
|
||||
*/
|
||||
findNextCourse(indexOffset: number)
|
||||
{
|
||||
return this.findCourse(this.activeIndex.split('/')[1], indexOffset);
|
||||
return this.findCourse(this.nav.info.id, indexOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -201,8 +135,22 @@ export default class Navigation extends Vue
|
||||
this.$emit('sign-out');
|
||||
break
|
||||
}
|
||||
case 'switch-dark':
|
||||
{
|
||||
App.instance.darkMode = !App.instance.darkMode;
|
||||
|
||||
if (this.isDark()) this.$cookies.set('dark', true);
|
||||
else this.$cookies.remove('dark');
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isDark()
|
||||
{
|
||||
return App.instance.darkMode;
|
||||
}
|
||||
|
||||
get version() {return Constants.VERSION}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div id="navigation">
|
||||
<el-menu style="margin-bottom: 10px;" class="centered" mode="horizontal"
|
||||
:default-active="activeIndex" @select="onSelect">
|
||||
:default-active="nav.id" @select="onSelect">
|
||||
|
||||
<div id="nav-title">
|
||||
<img id="nav-logo" alt="logo" src="../../assets/logo.png">
|
||||
@@ -11,11 +11,11 @@
|
||||
|
||||
<el-menu-item index="overall">Overall</el-menu-item>
|
||||
|
||||
<el-submenu index="courses">
|
||||
<el-submenu index="">
|
||||
<template slot="title">Courses</template>
|
||||
<el-menu-item v-for="course in courses"
|
||||
:index="formatCourseIndex(course)"
|
||||
:key="course.name">{{course.name}}</el-menu-item>
|
||||
:index="JSON.stringify(course.urlIndex)"
|
||||
:key="course.id">{{course.name}}</el-menu-item>
|
||||
</el-submenu>
|
||||
|
||||
<!-- Grading period selection -->
|
||||
@@ -26,8 +26,9 @@
|
||||
<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 3">Term 3</el-dropdown-item>
|
||||
<el-dropdown-item command="Term 4" disabled>Term 4</el-dropdown-item>
|
||||
<!-- TODO: Auto enable / disable quarters -->
|
||||
<el-dropdown-item command="All Year" divided>All Year</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
@@ -39,18 +40,20 @@
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item style="text-align: center">{{user.nickname}}</el-dropdown-item>
|
||||
|
||||
<el-dropdown-item icon="el-icon-sunrise" command="switch-dark" divided>{{!isDark() ? 'Dark Mode (Unfinished)' : 'Light Mode'}}</el-dropdown-item>
|
||||
|
||||
<el-dropdown-item icon="el-icon-switch-button" command="sign-out" divided>Sign Out</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</el-menu>
|
||||
|
||||
<!-- Previous course / Next course (Only when the page is courses) -->
|
||||
<div v-if="activeIndex.includes('course') && findNextCourse(-1) != null"
|
||||
<div v-if="nav.id === 'course' && findNextCourse(-1) != null"
|
||||
@click="nextCourse(-1)" id="prev-course" class="nav-course-operations unselectable">
|
||||
▲ PREVIOUS COURSE ▲
|
||||
</div>
|
||||
<footer>
|
||||
<div v-if="activeIndex.includes('course') && findNextCourse(1) != null"
|
||||
<div v-if="nav.id === 'course' && findNextCourse(1) != null"
|
||||
@click="nextCourse(1)" id="next-course" class="nav-course-operations unselectable">
|
||||
▼ NEXT COURSE ▼
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@ export default class Constants
|
||||
// static API_URL: string = 'http://localhost:24021/api';
|
||||
|
||||
/** Current version */
|
||||
static VERSION: string = '0.5.1.1229';
|
||||
static VERSION: string = '0.5.3.1373';
|
||||
|
||||
/** The minimum version that still supports the same cookies */
|
||||
static MIN_SUPPORTED_VERSION: string = '0.4.6.1087';
|
||||
|
||||
@@ -4,6 +4,8 @@ import Navigation from '@/components/navigation/navigation';
|
||||
import {GPAUtils} from '@/logic/utils/gpa-utils';
|
||||
import CacheUtils from '@/logic/utils/cache-utils';
|
||||
import Constants from '@/constants';
|
||||
import {Index} from '@/logic/nav-controller';
|
||||
import App from '@/components/app/app';
|
||||
|
||||
/**
|
||||
* Objects of this interface represent assignment grades.
|
||||
@@ -27,6 +29,9 @@ export class Assignment
|
||||
|
||||
gradingPeriod: number;
|
||||
|
||||
// Callbacks when this object updates
|
||||
private updateCallbacks: (() => void)[] = [];
|
||||
|
||||
/**
|
||||
* Construct assignment with json object
|
||||
*
|
||||
@@ -57,7 +62,67 @@ export class Assignment
|
||||
*/
|
||||
get graded()
|
||||
{
|
||||
return this.complete == 'Complete';
|
||||
// TODO: Add more cases
|
||||
return this.include && (this.complete == 'Complete' || this.complete == 'Late' || this.complete == 'NREQ');
|
||||
}
|
||||
|
||||
/**
|
||||
* What is the problem with this assignment
|
||||
*
|
||||
* @return string Empty string if complete, otherwise return problem.
|
||||
*/
|
||||
get problem()
|
||||
{
|
||||
switch (this.complete)
|
||||
{
|
||||
case 'Complete': return '';
|
||||
case 'Late': return 'Late';
|
||||
case 'NREQ': return 'Dropped';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the text color of the problem
|
||||
*/
|
||||
get problemColor()
|
||||
{
|
||||
switch (this.complete)
|
||||
{
|
||||
case 'Late': return '#ff0036';
|
||||
case 'NREQ': return '#41b141';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add callback
|
||||
*
|
||||
* @param callback
|
||||
*/
|
||||
addCallback(callback: () => void)
|
||||
{
|
||||
this.updateCallbacks.push(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark as read
|
||||
*/
|
||||
markAsRead(): Promise<void>
|
||||
{
|
||||
return new Promise((resolve, reject) => {
|
||||
App.http.post('/mark-as-read', {scoreId: this.scoreId})
|
||||
.then(response =>
|
||||
{
|
||||
// Check success
|
||||
if (response.success)
|
||||
{
|
||||
this.unread = false;
|
||||
this.updateCallbacks.forEach(callback => callback());
|
||||
resolve();
|
||||
}
|
||||
else reject(response.data);
|
||||
})
|
||||
.catch(reject)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,12 +248,22 @@ export default class Course
|
||||
{
|
||||
return this.cache.get('GradingPeriods', () =>
|
||||
{
|
||||
let timeCode = Navigation.instance.getSelectedTerm();
|
||||
return (timeCode == -1 ? [0, 1, 2, 3] : [timeCode]).filter(term =>
|
||||
return (this.rawSelectedTerm == -1 ? [0, 1, 2, 3] : [this.rawSelectedTerm]).filter(term =>
|
||||
this.termAssignments[term].filter(a => a.graded).length != 0);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get currently selected grading periods
|
||||
*/
|
||||
get allGradingPeriods(): number[]
|
||||
{
|
||||
return this.cache.get('AllGradingPeriods', () =>
|
||||
{
|
||||
return [0, 1, 2, 3].filter(term => this.termAssignments[term].filter(a => a.graded).length != 0);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get assignments of the selected grading periods
|
||||
*/
|
||||
@@ -228,11 +303,33 @@ export default class Course
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get letter grade by term
|
||||
*
|
||||
* @param term
|
||||
*/
|
||||
letterGradeTerm(term: number): string
|
||||
{
|
||||
return this.cache.get('LetterGrade' + term, () =>
|
||||
{
|
||||
// Get scale
|
||||
let scale = GPAUtils.findScale(this.numericGradeTerm(term));
|
||||
|
||||
// Scale not found
|
||||
return scale == undefined ? '--' : scale.letter;
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get numeric grade
|
||||
*/
|
||||
get numericGrade()
|
||||
{
|
||||
return this.gradingPeriods
|
||||
.map(term => this.numericGradeTerm(term)).reduce((p, v) => p + v)
|
||||
/ this.gradingPeriods.length;
|
||||
return this.cache.get('NumericGrade', () =>
|
||||
{
|
||||
return this.gradingPeriods.map(term => this.numericGradeTerm(term))
|
||||
.reduce((p, v) => p + v) / this.gradingPeriods.length
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -294,4 +391,28 @@ export default class Course
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get url hash code
|
||||
*/
|
||||
get urlHash(): string
|
||||
{
|
||||
return `course/${this.id}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Get navigation index
|
||||
*/
|
||||
get urlIndex(): Index
|
||||
{
|
||||
return {hash: this.urlHash, title: this.name, identifier: 'course', info: {id: this.id}}
|
||||
}
|
||||
|
||||
/**
|
||||
* Selected term
|
||||
*/
|
||||
get rawSelectedTerm(): number
|
||||
{
|
||||
return Navigation.instance.getSelectedTerm()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
import {FormatUtils} from '@/logic/utils/format-utils';
|
||||
import pWaitFor from 'p-wait-for';
|
||||
import App from '@/components/app/app';
|
||||
|
||||
export interface Index
|
||||
{
|
||||
hash: string
|
||||
title?: string
|
||||
identifier: string
|
||||
info?: any
|
||||
}
|
||||
|
||||
export default class NavController
|
||||
{
|
||||
// Current index
|
||||
index: Index;
|
||||
|
||||
// Callback
|
||||
updateCallback?: () => void;
|
||||
|
||||
constructor()
|
||||
{
|
||||
// Create history state listener
|
||||
window.onpopstate = (e: any) =>
|
||||
{
|
||||
if (e.state)
|
||||
{
|
||||
// Restore previous tab
|
||||
//console.log(`onPopState: Current: ${this.index.hash}, Previous: ${e.state.hash}`);
|
||||
this.updateIndex(e.state, false);
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize
|
||||
this.init()
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize from last location
|
||||
*/
|
||||
private init()
|
||||
{
|
||||
if (window.location.hash == '#info') return;
|
||||
|
||||
// Check history from last session
|
||||
if (window.history.state != undefined && window.history.state.hash != undefined)
|
||||
{
|
||||
// Last history exists
|
||||
this.index = window.history.state;
|
||||
return;
|
||||
}
|
||||
|
||||
// Last history doesn't exist but hash url might exist
|
||||
let hash = window.location.hash.replace('#', '');
|
||||
|
||||
// Check hash
|
||||
if (hash == '')
|
||||
{
|
||||
// No location info in url, set page to overall
|
||||
window.history.replaceState(this.convertIndex('overall'), '', '/#overall');
|
||||
this.updateIndex('overall', false);
|
||||
return;
|
||||
}
|
||||
|
||||
// There is hash info in url
|
||||
let split = hash.split('/');
|
||||
|
||||
// Not course -> don't know what to do with this url, so just refresh
|
||||
if (split[0] != 'course')
|
||||
{
|
||||
this.initClear();
|
||||
return;
|
||||
}
|
||||
|
||||
// Is course -> Update index with placeholder title
|
||||
this.updateIndex({hash: hash, title: `Loading...`, identifier: 'course', info: {id: +split[1]}}, false);
|
||||
|
||||
// Wait for courses to finish loading
|
||||
pWaitFor(() => App.instance != undefined && App.instance.assignmentsReady).then(() =>
|
||||
{
|
||||
// Find course
|
||||
let course = App.instance.courses.find(c => c.id == +split[1]);
|
||||
|
||||
// This person has no such course, refresh to overall
|
||||
if (course == null)
|
||||
{
|
||||
this.initClear();
|
||||
return;
|
||||
}
|
||||
|
||||
window.history.replaceState(course.urlIndex, '', '/#' + course.urlHash);
|
||||
this.updateIndex(course.urlIndex, false);
|
||||
})
|
||||
}
|
||||
|
||||
private initClear()
|
||||
{
|
||||
window.location.hash = '';
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update index
|
||||
*
|
||||
* @param index Hash and title | Hash only
|
||||
* @param history Record in history or not (Default true)
|
||||
*/
|
||||
updateIndex(index: Index | string, history: boolean = true)
|
||||
{
|
||||
index = this.convertIndex(index);
|
||||
|
||||
// Call custom event
|
||||
if (this.updateCallback != null) this.updateCallback();
|
||||
|
||||
// Record history or not
|
||||
if (history)
|
||||
{
|
||||
//console.log(`history: Current: ${this.index.hash}, New: ${index.hash}`);
|
||||
|
||||
// Check url
|
||||
let url = `/#${index.hash}`;
|
||||
|
||||
// Push history state
|
||||
window.history.pushState(index, '', url);
|
||||
}
|
||||
|
||||
// Update title
|
||||
document.title = 'Veracross Analyzer - ' + index.title;
|
||||
|
||||
// Scroll to top
|
||||
window.scrollTo(0, 0);
|
||||
|
||||
// Update selected index
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check index conversion
|
||||
*
|
||||
* @param index Hash and title | Hash only
|
||||
* @return Index Hash and title
|
||||
*/
|
||||
private convertIndex(index: Index | string): Index
|
||||
{
|
||||
// Convert index format if it is hash only
|
||||
if (typeof index == 'string') index = {hash: index, identifier: index};
|
||||
if (index.title == null) index.title = FormatUtils.toTitleCase(index.hash);
|
||||
return index;
|
||||
}
|
||||
|
||||
get id(): string
|
||||
{
|
||||
return this.index.identifier
|
||||
}
|
||||
|
||||
get info(): any
|
||||
{
|
||||
return this.index.info
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import Course from '@/logic/course';
|
||||
import Navigation from '@/components/navigation/navigation';
|
||||
import Constants from '@/constants';
|
||||
|
||||
@@ -20,17 +19,6 @@ UNKNOWN_COURSE_LIST.set('Painting', LEVEL_CP);
|
||||
|
||||
export class CourseUtils
|
||||
{
|
||||
/**
|
||||
* Format course to tab index string
|
||||
*
|
||||
* @param course Course object
|
||||
* @return string Tab index
|
||||
*/
|
||||
public static formatTabIndex(course: Course): string
|
||||
{
|
||||
return `course/${course.id}/${course.name.toLowerCase().split(' ').join('-')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect course level based on course name
|
||||
*
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Constants from '@/constants';
|
||||
import App from '@/components/app/app';
|
||||
|
||||
export default class GraphUtils
|
||||
{
|
||||
@@ -54,6 +55,9 @@ export default class GraphUtils
|
||||
*/
|
||||
static getGradeMarkAreas(opacity: number)
|
||||
{
|
||||
// TODO: Auto update after switching dark mode (possibly by refreshing)
|
||||
opacity = App.instance.darkMode ? 0.1 : opacity;
|
||||
|
||||
return {
|
||||
silent: true,
|
||||
data:
|
||||
|
||||
@@ -57,6 +57,13 @@
|
||||
text-align: right;
|
||||
float: right;
|
||||
|
||||
// Status / Problems
|
||||
span.status
|
||||
{
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
// Percentage score
|
||||
span.percent
|
||||
{
|
||||
font-style: italic;
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
</el-col>
|
||||
|
||||
<el-col :span="6" class="grade">
|
||||
<span v-if="assignment.problem" class="status entry-box" :style="{color: assignment.problemColor}">
|
||||
{{assignment.problem}}
|
||||
</span>
|
||||
<span class="percent entry-box">
|
||||
{{(assignment.score / assignment.scoreMax * 100).toFixed(1)}}
|
||||
<span class="symbol">%</span>
|
||||
|
||||
@@ -1,27 +1,30 @@
|
||||
// Main card content
|
||||
.course-card-content.main
|
||||
{
|
||||
padding: 0 20px 0 20px;
|
||||
height: 90px;
|
||||
|
||||
// Main color
|
||||
background: white;
|
||||
|
||||
// Alignment
|
||||
display: block;
|
||||
|
||||
padding: 20px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.course-col-name
|
||||
#block-info
|
||||
{
|
||||
// Align left
|
||||
text-align: left;
|
||||
float: left;
|
||||
|
||||
.course-name
|
||||
#name
|
||||
{
|
||||
overflow: hidden;
|
||||
font-size: 22px;
|
||||
color: var(--main);
|
||||
}
|
||||
|
||||
.course-teacher
|
||||
#teacher
|
||||
{
|
||||
font-size: 12px;
|
||||
color: #999999;
|
||||
@@ -29,7 +32,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.course-col-grade
|
||||
#block-grade
|
||||
{
|
||||
// Align right
|
||||
text-align: right;
|
||||
@@ -37,17 +40,18 @@
|
||||
|
||||
// Adjust position
|
||||
margin-top: -2px;
|
||||
margin-left: 10px;
|
||||
|
||||
.course-grade
|
||||
#grade
|
||||
{
|
||||
font-size: 21px;
|
||||
}
|
||||
|
||||
.course-updates
|
||||
#updates
|
||||
{
|
||||
font-size: 14px;
|
||||
|
||||
.unread-number
|
||||
#unread-number
|
||||
{
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
@@ -60,33 +64,53 @@
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.unread-text
|
||||
#unread-text
|
||||
{
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
.course-updates.unread
|
||||
#updates.unread
|
||||
{
|
||||
.unread-number
|
||||
#unread-number
|
||||
{
|
||||
background: var(--unread);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.unread-text
|
||||
#unread-text
|
||||
{
|
||||
color: var(--unread);
|
||||
}
|
||||
}
|
||||
|
||||
.course-updates.none
|
||||
#updates.none
|
||||
{
|
||||
color: #999999;
|
||||
|
||||
.unread-number
|
||||
#unread-number
|
||||
{
|
||||
background: #eeeeee;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#block-term-grades
|
||||
{
|
||||
// Align right
|
||||
width: auto;
|
||||
float: right;
|
||||
margin-right: 10px;
|
||||
|
||||
color: gray;
|
||||
|
||||
#term, #term-numeric
|
||||
{
|
||||
font-size: 11px;
|
||||
color: #b3b3b3;
|
||||
}
|
||||
|
||||
#term-letter
|
||||
{
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,34 @@
|
||||
<template>
|
||||
<div id="course-head" class="course-card-content main vertical-center" @click="redirect">
|
||||
<el-row>
|
||||
<el-col :span="12" class="course-col-name">
|
||||
<div v-if="clickable" class="course-name clickable" @click="redirect">{{course.name}}</div>
|
||||
<div v-if="!clickable" class="course-name">{{course.name}}</div>
|
||||
<div class="course-teacher">{{course.teacherName}}</div>
|
||||
</el-col>
|
||||
<el-col :span="12" class="course-col-grade">
|
||||
<div class="course-grade">
|
||||
<span class="letter">{{course.letterGrade}} </span>
|
||||
<span class="numeric">{{course.numericGrade === undefined ? '--' : course.numericGrade.toFixed(2)}}</span>
|
||||
<span class="percent" v-if="course.numericGrade !== undefined">%</span>
|
||||
</div>
|
||||
<div class="course-updates" @click="redirect" :class="unread === 0 ? 'none' : 'unread'">
|
||||
<span class="unread-number">{{unread}}</span>
|
||||
<span class="unread-text" :class="clickable ? 'clickable' : ''">
|
||||
new update{{unread >= 2 ? 's' : ''}}
|
||||
</span>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div id="course-head" class="course-card-content main vertical-center"
|
||||
:class="clickable ? 'clickable' : ''" @click="redirect">
|
||||
<div id="block-info">
|
||||
<div id="name">{{course.name}}</div>
|
||||
<div id="teacher">{{course.teacherName}}</div>
|
||||
</div>
|
||||
<div id="block-grade">
|
||||
<div id="grade">
|
||||
<span id="letter">{{course.letterGrade}} </span>
|
||||
<span id="numeric">{{course.numericGrade === undefined ? '--' : course.numericGrade.toFixed(2)}}</span>
|
||||
<span id="percent" v-if="course.numericGrade !== undefined">%</span>
|
||||
</div>
|
||||
<div id="updates" @click="redirect" :class="unread === 0 ? 'none' : 'unread'">
|
||||
<span id="unread-number">{{unread}}</span>
|
||||
<span id="unread-text" :class="clickable ? 'clickable' : ''">new update{{unread >= 2 ? 's' : ''}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="block-term-grades" v-if="course.rawSelectedTerm === -1"
|
||||
v-for="term in course.allGradingPeriods.slice().reverse()">
|
||||
<div id="term">Term {{term + 1}}</div>
|
||||
<div id="term-letter">{{course.letterGradeTerm(term)}}</div>
|
||||
<div id="term-numeric">{{course.numericGradeTerm(term).toFixed(1)}}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import Course from '@/logic/course';
|
||||
import {CourseUtils} from '@/logic/utils/course-utils';
|
||||
import Navigation from '@/components/navigation/navigation';
|
||||
import App from '@/components/app/app';
|
||||
|
||||
@Component
|
||||
export default class CourseHead extends Vue
|
||||
@@ -44,9 +45,9 @@
|
||||
redirect()
|
||||
{
|
||||
if (!this.clickable) return;
|
||||
Navigation.instance.updateIndex(CourseUtils.formatTabIndex(this.course));
|
||||
App.instance.nav.updateIndex(this.course.urlIndex);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style src="./course-head.scss" lang="scss"/>
|
||||
<style src="./course-head.scss" lang="scss" scoped/>
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import App from '@/components/app/app';
|
||||
import AssignmentEntry from '@/pages/overall/overall-course/assignment-entry/assignment-entry.vue';
|
||||
import CourseHead from '@/pages/overall/overall-course/course-head/course-head.vue';
|
||||
import Course, {Assignment} from '@/logic/course';
|
||||
|
||||
@Component({
|
||||
components: {UnreadEntry: AssignmentEntry, CourseHead}
|
||||
})
|
||||
export default class OverallCourse extends Vue
|
||||
{
|
||||
@Prop({required: true}) course: Course;
|
||||
|
||||
private unread: number = -1;
|
||||
private unreadAssignments: Assignment[] = [];
|
||||
|
||||
/**
|
||||
* Count the number of unread assignments with cache
|
||||
*/
|
||||
countUnread(): number
|
||||
{
|
||||
if (this.unread == -1)
|
||||
{
|
||||
this.unreadAssignments = this.course.assignments.filter(a => a.unread);
|
||||
return this.unread = this.unreadAssignments.length;
|
||||
}
|
||||
else return this.unread;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark an assignment as read
|
||||
*/
|
||||
markAsRead(assignment: Assignment)
|
||||
{
|
||||
App.http.post('/mark-as-read', {scoreId: assignment.scoreId})
|
||||
.then(response =>
|
||||
{
|
||||
// Check success
|
||||
if (response.success)
|
||||
{
|
||||
this.unreadAssignments = this.unreadAssignments.filter(a => a != assignment);
|
||||
this.unread = this.unreadAssignments.length;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Show error message TODO: Show it properly
|
||||
alert(response.data)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,47 @@
|
||||
<template>
|
||||
<div id="overall-course">
|
||||
<el-card class="course-card">
|
||||
<course-head :clickable="true" :course="course" :unread="countUnread()"/>
|
||||
<course-head :clickable="true" :course="course" :unread="unread()"/>
|
||||
<div class="course-card-content expand"
|
||||
v-if="countUnread() !== 0">
|
||||
<unread-entry v-for="assignment in unreadAssignments"
|
||||
v-if="unread() !== 0">
|
||||
<assignment-entry v-for="assignment in unreadAssignments()"
|
||||
:assignment="assignment"
|
||||
:key="assignment.id"
|
||||
unread="true"
|
||||
v-on:mark-as-read="markAsRead">
|
||||
</unread-entry>
|
||||
v-on:mark-as-read="assignment.markAsRead()">
|
||||
</assignment-entry>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./overall-course.ts" lang="ts"></script>
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import AssignmentEntry from '@/pages/overall/overall-course/assignment-entry/assignment-entry.vue';
|
||||
import CourseHead from '@/pages/overall/overall-course/course-head/course-head.vue';
|
||||
import Course, {Assignment} from '@/logic/course';
|
||||
|
||||
@Component({
|
||||
components: {AssignmentEntry, CourseHead}
|
||||
})
|
||||
export default class OverallCourse extends Vue
|
||||
{
|
||||
@Prop({required: true}) course: Course;
|
||||
|
||||
mounted()
|
||||
{
|
||||
this.unreadAssignments().forEach(a => a.addCallback(() => this.$forceUpdate()));
|
||||
}
|
||||
|
||||
unreadAssignments(): Assignment[]
|
||||
{
|
||||
return this.course.assignments.filter(a => a.unread);
|
||||
}
|
||||
|
||||
unread(): number
|
||||
{
|
||||
return this.unreadAssignments().length;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style src="./overall-course.scss" lang="scss" scoped/>
|
||||
|
||||
@@ -51,3 +51,10 @@
|
||||
margin-right: 20px;
|
||||
min-width: 170px;
|
||||
}
|
||||
|
||||
.dialog-checkbox
|
||||
{
|
||||
display: block;
|
||||
margin-top: 20px;
|
||||
margin-bottom: -20px;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
<template>
|
||||
<div id="overall">
|
||||
<el-progress v-if="started" :text-inside="true" :percentage="progress()"
|
||||
:stroke-width="20" status="success" style="margin: 0 20px"/>
|
||||
|
||||
<el-dialog title="Notice" :visible.sync="clearUnreadPrompt"
|
||||
width="30%" style="word-break: unset;">
|
||||
<span>You have too many new grade notifications. Clear them now?</span>
|
||||
<img src="./too-many-unread.png" alt=""/>
|
||||
<el-checkbox class="dialog-checkbox" v-model="dontAskAgain">Don't Ask Again</el-checkbox>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="clearUnread(false)" style="float: left">Nope</el-button>
|
||||
<el-button type="primary" @click="clearUnread(true)">Sure!</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
|
||||
<el-row v-if="getGPA().gpa !== -1">
|
||||
<el-col :span="4" class="overall-span">
|
||||
<el-card class="large gpa-card vertical-center" body-style="padding: 0">
|
||||
@@ -42,8 +56,8 @@
|
||||
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 OverallCourse from '@/pages/overall/overall-course/overall-course.vue';
|
||||
import Course, {Assignment} from '@/logic/course';
|
||||
import {GPAUtils} from '@/logic/utils/gpa-utils';
|
||||
|
||||
@Component({
|
||||
@@ -57,10 +71,72 @@
|
||||
* This function is called to get gpa since I can't import another
|
||||
* class in the Vue file.
|
||||
*/
|
||||
public getGPA()
|
||||
getGPA()
|
||||
{
|
||||
return GPAUtils.getGPA(this.courses);
|
||||
}
|
||||
|
||||
// For clear unread prompt
|
||||
unread: Assignment[];
|
||||
clearUnreadPrompt = false;
|
||||
dontAskAgain = false;
|
||||
started = false;
|
||||
|
||||
/**
|
||||
* Mark as read progress
|
||||
*/
|
||||
progress()
|
||||
{
|
||||
return +(this.unread.filter(a => !a.unread).length / this.unread.length * 100).toFixed(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* On page load - check if the user has too many notifications
|
||||
*/
|
||||
mounted()
|
||||
{
|
||||
// Check unread
|
||||
if (!this.$cookies.isKey('va.ignore-unread'))
|
||||
{
|
||||
// Count unread
|
||||
this.unread = this.courses.flatMap(c => c.assignments.filter(a => a.unread));
|
||||
|
||||
// Prompt clear
|
||||
if (this.unread.length > 15)
|
||||
{
|
||||
this.clearUnreadPrompt = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear unread
|
||||
*
|
||||
* @param confirmed
|
||||
*/
|
||||
clearUnread(confirmed: boolean)
|
||||
{
|
||||
// Hide prompt
|
||||
this.clearUnreadPrompt = false;
|
||||
|
||||
// Not confirmed, do nothing
|
||||
if (!confirmed)
|
||||
{
|
||||
if (!this.dontAskAgain) return;
|
||||
|
||||
// Don't ask again
|
||||
this.$cookies.set('va.ignore-unread', true);
|
||||
}
|
||||
|
||||
// Clear unread
|
||||
this.started = true;
|
||||
this.unread.forEach((a, i) =>
|
||||
{
|
||||
// Delay: 100ms per assignment
|
||||
// I don't want my server to explode lol
|
||||
setTimeout(() => a.markAsRead().then(() => this.$forceUpdate()), 100 * i);
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div id="info">
|
||||
<div id="top">
|
||||
<div id="title">Veracross Analyzer for SJP</div>
|
||||
<div id="subtitle">Know your grades better.</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Vue} from 'vue-property-decorator';
|
||||
|
||||
@Component
|
||||
export default class Info extends Vue
|
||||
{
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
#top
|
||||
{
|
||||
padding: 40vh 0;
|
||||
|
||||
// Center and scale the image nicely
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
|
||||
text-align: right;
|
||||
|
||||
#title
|
||||
{
|
||||
font-size: 30px;
|
||||
margin-right: 10vw;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#subtitle
|
||||
{
|
||||
margin-right: 10vw;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -2,7 +2,9 @@
|
||||
"defaultSeverity": "warning",
|
||||
"linterOptions": {
|
||||
"exclude": [
|
||||
"node_modules/**"
|
||||
"node_modules/**",
|
||||
"*.json",
|
||||
"**/*.json"
|
||||
]
|
||||
},
|
||||
"rules": {
|
||||
|
||||