Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b41296507 |
@@ -1,21 +0,0 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 HyDEV
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,56 +0,0 @@
|
||||
<h1 align="center"><br><br>
|
||||
VeracrossAnalyzer UI
|
||||
</h1>
|
||||
|
||||
<h4 align="center">
|
||||
A Website, A Visual Representation of Students' Grade Data on Veracross
|
||||
</h4>
|
||||
|
||||
<h5 align="center">
|
||||
<a href="#intro">Introduction</a>
|
||||
<a href="#setup">Project Setup</a>
|
||||
<a href="#license">License</a>
|
||||
</h5><br><br><br>
|
||||
|
||||
|
||||
|
||||
<a name="intro"></a>
|
||||
Introduction:
|
||||
--------
|
||||
|
||||
This is a website that generates visual representation of students' grade data on Veracross. Currently there is only one graph and one numerical data representing the GPA. But also it just released yesterday! (Yay!) What do you expect this soon lol?
|
||||
|
||||
**Here's how it looks like right now:** *(Now all of you know my grades ;-;)*
|
||||
|
||||

|
||||
|
||||
<br>
|
||||
|
||||
<a name="setup"></a>
|
||||
Project Setup:
|
||||
--------
|
||||
|
||||
TODO: Actually write a project setup tutorial that's not generated by Vue on initialization ;-;.
|
||||
|
||||
### Install
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
npm run serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
<br>
|
||||
|
||||
<a name="license"></a>
|
||||
License: [MIT](https://choosealicense.com/licenses/mit/)
|
||||
--------
|
||||
|
||||
The MIT license basically means that this project is open-soucred and you can do whatever you want with it, as long as you include a copy of this license in your distribution. You don't have to ask for permissions to use or anything. However, if you do bad things with it, I'm not responsible.
|
||||
@@ -1,5 +0,0 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/app'
|
||||
]
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
echo "Switch to production database settings"
|
||||
exit
|
||||
|
||||
# abort on errors
|
||||
set -e
|
||||
|
||||
# build
|
||||
npm run build
|
||||
|
||||
# navigate into the build output directory
|
||||
cd dist
|
||||
|
||||
# if you are deploying to a custom domain
|
||||
echo 'demo.vera.hydev.org' > CNAME
|
||||
|
||||
git init
|
||||
git add -A
|
||||
git commit -m 'deploy'
|
||||
|
||||
# if you are deploying to https://<USERNAME>.github.io/<REPO>
|
||||
git push -f git@github.com:Hykilpikonna/VeracrossAnalyzerDemo.git master:gh-pages
|
||||
|
||||
cd -
|
||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
@@ -0,0 +1,5 @@
|
||||
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge"><meta name=viewport content="width=1024"><link rel=icon href=/logo@32px.png><title>Veracross Analyzer</title><link href=/css/app.72ceade9.css rel=preload as=style><link href=/js/app.c7702e9a.js rel=preload as=script><link href=/js/chunk-vendors.4383782d.js rel=preload as=script><link href=/css/app.72ceade9.css rel=stylesheet></head><body style="margin: 0"><noscript><strong>We're sorry but veracross-analyzer doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id=app></div><script src=https://cdn.jsdelivr.net/npm/echarts/dist/echarts.min.js></script><script src=https://cdn.jsdelivr.net/npm/v-charts/lib/index.min.js></script><link rel=stylesheet href=https://cdn.jsdelivr.net/npm/v-charts/lib/style.min.css><link rel=stylesheet href=https://unpkg.com/element-ui/lib/theme-chalk/index.css><link href="https://fonts.googleapis.com/css?family=Nunito+Sans&display=swap" rel=stylesheet><script src=/js/chunk-vendors.4383782d.js></script><script src=/js/app.c7702e9a.js></script></body><script async src="https://www.googletagmanager.com/gtag/js?id=G-Q615K1KFLC"></script><script>window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', 'G-Q615K1KFLC');</script></html>
|
||||
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"name": "veracross-analyzer",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/chroma-js": "^2.0.0",
|
||||
"@types/md5": "^2.2.0",
|
||||
"chroma-js": "^2.1.0",
|
||||
"core-js": "^2.6.10",
|
||||
"echarts": "^4.8.0",
|
||||
"element-ui": "^2.13.2",
|
||||
"md5": "^2.2.1",
|
||||
"moment": "^2.27.0",
|
||||
"p-wait-for": "^3.1.0",
|
||||
"v-charts": "^1.19.0",
|
||||
"vue": "^2.6.11",
|
||||
"vue-class-component": "^7.2.5",
|
||||
"vue-cookies": "^1.7.3",
|
||||
"vue-property-decorator": "^8.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "^3.12.1",
|
||||
"@vue/cli-plugin-typescript": "^3.12.1",
|
||||
"@vue/cli-service": "^4.4.6",
|
||||
"node-sass": "^4.14.1",
|
||||
"sass-loader": "^8.0.2",
|
||||
"typescript": "^3.9.7",
|
||||
"vue-template-compiler": "^2.6.11"
|
||||
},
|
||||
"postcss": {
|
||||
"plugins": {
|
||||
"autoprefixer": {}
|
||||
}
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions"
|
||||
]
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<!--meta name="viewport" content="width=device-width,initial-scale=1.0"-->
|
||||
<meta name="viewport" content="width=1024">
|
||||
|
||||
<link rel="icon" href="<%= BASE_URL %>logo@32px.png">
|
||||
<title>Veracross Analyzer</title>
|
||||
</head>
|
||||
|
||||
<body style="margin: 0">
|
||||
<noscript>
|
||||
<strong>We're sorry but veracross-analyzer doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
|
||||
<div id="app"></div>
|
||||
|
||||
<!-- V-Charts -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/echarts/dist/echarts.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/v-charts/lib/index.min.js"></script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/v-charts/lib/style.min.css">
|
||||
|
||||
<!-- ElementUI -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
|
||||
|
||||
<!-- Fonts -->
|
||||
<link href="https://fonts.googleapis.com/css?family=Nunito+Sans&display=swap" rel="stylesheet">
|
||||
</body>
|
||||
|
||||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Q615K1KFLC"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', 'G-Q615K1KFLC');
|
||||
</script>
|
||||
</html>
|
||||
@@ -1,45 +0,0 @@
|
||||
import LoginUser from '@/logic/login-user';
|
||||
import App from '@/components/app/app';
|
||||
import pWaitFor from 'p-wait-for';
|
||||
|
||||
export default class AppDemo
|
||||
{
|
||||
static loadDemo(app: App)
|
||||
{
|
||||
app.logLoading('1. Logging in...')
|
||||
App.http.get('./demo-data/token.json').then(response =>
|
||||
{
|
||||
app.user = new LoginUser(response.data)
|
||||
app.courses = app.user.courses
|
||||
|
||||
app.logLoading('1. Loading assignments...')
|
||||
app.courses.forEach(course =>
|
||||
{
|
||||
App.http.get(`./demo-data/assignments-${course.assignmentsId}.json`).then(response =>
|
||||
{
|
||||
course.loadAssignments(response.data);
|
||||
})
|
||||
})
|
||||
|
||||
pWaitFor(() => app.courses.every(c => c.rawAssignments)).then(() =>
|
||||
{
|
||||
app.gradedCourses = app.courses.filter(c => c.isGraded)
|
||||
app.gradedCourses.forEach(c => [0, 1, 2, 3].forEach(i => c.termGrading[i] = {method: 'TOTAL_MEAN', weightingMap: {}}))
|
||||
app.logLoading('');
|
||||
app.assignmentsReady = true
|
||||
app.showRating = true
|
||||
|
||||
app.$alert(
|
||||
'This demo analyzes an offline snapshot of my data from Jun 6, 2020, ' +
|
||||
'which displays my academic results from Junior year.<br/>' +
|
||||
'<br/>' +
|
||||
'Feel free to click around! 😇<br/>' +
|
||||
'<br/>' +
|
||||
'-- The Veracross Analyzer Team (YGui)<br/>' +
|
||||
'-- Made with 🧡 in SJP',
|
||||
'🥳 Welcome to VeracrossAnalyzer Demo!',
|
||||
{dangerouslyUseHTMLString: true, confirmButtonText: 'OK'});
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
div
|
||||
{
|
||||
font-family: -apple-system, Nunito Sans, Avenir, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
|
||||
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue,
|
||||
Hiragino Sans GB, Microsoft YaHei, WenQuanYi Micro Hei, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
#app
|
||||
{
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-align: center;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
#app-content
|
||||
{
|
||||
// Limit max width
|
||||
max-width: 1300px;
|
||||
text-align: center;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.theme-default
|
||||
{
|
||||
--unread: #ff6c00;
|
||||
--main: #0c6dad;
|
||||
|
||||
--assignment-type-2: #3f991e;
|
||||
--assignment-type-3: #ff9900;
|
||||
--assignment-type-4: #b02b02;
|
||||
}
|
||||
|
||||
.dark
|
||||
{
|
||||
--dark-layer-1: #383838;
|
||||
--dark-layer-2: #525252;
|
||||
--dark-layer-3: #6c6c6c;
|
||||
--dark-foreground: #e9e9e9;
|
||||
|
||||
background: var(--dark-layer-1) !important;
|
||||
|
||||
div, ul
|
||||
{
|
||||
background: var(--dark-layer-2) !important;
|
||||
color: var(--dark-foreground) !important;
|
||||
}
|
||||
|
||||
span, button
|
||||
{
|
||||
color: var(--dark-foreground) !important;
|
||||
}
|
||||
|
||||
.el-card
|
||||
{
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
// Overall
|
||||
#app-inner, #overall, #overall-course, .overall-span, #app-content
|
||||
{
|
||||
background: var(--dark-layer-1) !important;
|
||||
}
|
||||
|
||||
// Course card
|
||||
.entry-box, .none .unread-number {background: #797979 !important}
|
||||
.entry-box.max {background-color: #949494 !important}
|
||||
.entry-box.percent {background-color: #a7a490 !important}
|
||||
.course-name {color: #cffff6 !important}
|
||||
#block-grade #updates.none #unread-number {background: #757575 !important}
|
||||
|
||||
.course-card-content.expand, .assignment-entry, .unread-row,
|
||||
.unread-row .el-col, #assignment-type-head, .course-page-graph.el-col
|
||||
{
|
||||
background-color: var(--dark-layer-3) !important;
|
||||
}
|
||||
|
||||
// Nav bar
|
||||
.el-menu--horizontal>.el-menu-item.is-active {color: var(--dark-foreground) !important;}
|
||||
}
|
||||
|
||||
// ##############
|
||||
// # Global CSS #
|
||||
// ##############
|
||||
|
||||
.el-card
|
||||
{
|
||||
margin: 10px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.el-card.large
|
||||
{
|
||||
height: 494px;
|
||||
}
|
||||
|
||||
// Fix padding
|
||||
.el-card__body
|
||||
{
|
||||
padding-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
|
||||
// Vertical centering
|
||||
.vertical-center
|
||||
{
|
||||
// Vertical center
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
// Remove card padding for styling issues
|
||||
div.el-card.course-card > div.el-card__body
|
||||
{
|
||||
padding-right: 0 !important;
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
||||
// Clickable text
|
||||
.clickable:hover
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
.el-dropdown-menu__item
|
||||
{
|
||||
font-family: Nunito Sans, Helvetica Neue, Microsoft YaHei, "微软雅黑", Arial, sans-serif;
|
||||
}
|
||||
|
||||
// Fix word breaking
|
||||
.el-dialog__body
|
||||
{
|
||||
word-break: unset !important;
|
||||
}
|
||||
|
||||
.comic
|
||||
{
|
||||
font-family: "Comic Sans MS", Nunito Sans, Helvetica Neue, Microsoft YaHei, "微软雅黑", Arial, sans-serif;
|
||||
}
|
||||
|
||||
#demo-not-available
|
||||
{
|
||||
padding-top: 40vh;
|
||||
font-size: 2em;
|
||||
color: #bbbbbb;
|
||||
margin: 0 50px;
|
||||
}
|
||||
@@ -1,274 +0,0 @@
|
||||
import {Component, Vue} from 'vue-property-decorator';
|
||||
import Login from '@/components/login/login.vue';
|
||||
import Navigation from '@/components/navigation/navigation.vue';
|
||||
import Overall from '@/pages/overall/overall.vue';
|
||||
import Constants from '@/constants';
|
||||
import pWaitFor from 'p-wait-for';
|
||||
import {HttpUtils} from '@/logic/utils/http-utils';
|
||||
import Loading from '@/components/overlays/loading.vue';
|
||||
import CoursePage from '@/pages/course/course-page.vue';
|
||||
import Course from '@/logic/course';
|
||||
import LoginUser from '@/logic/login-user';
|
||||
import NavController from '@/logic/nav-controller';
|
||||
import Info from '@/statics/Info.vue';
|
||||
import CourseSelection from '@/pages/course-selection/course-selection.vue';
|
||||
import AppDemo from '@/components/app/app-demo';
|
||||
|
||||
@Component({
|
||||
components: {Login, Navigation, Overall, Loading, CoursePage, Info, CourseSelection},
|
||||
})
|
||||
export default class App extends Vue
|
||||
{
|
||||
// List of course that the student takes
|
||||
courses: Course[] = [];
|
||||
gradedCourses: Course[] = [];
|
||||
|
||||
// Are the course assignments loaded from the server.
|
||||
assignmentsReady: boolean = false;
|
||||
|
||||
// Token
|
||||
user: LoginUser = null as any;
|
||||
|
||||
// Loading text
|
||||
loading: string = '';
|
||||
|
||||
// Loading error
|
||||
loadingError: boolean = false;
|
||||
|
||||
// Navigation controller
|
||||
nav: NavController = new NavController();
|
||||
|
||||
// Http Client
|
||||
static http: HttpUtils = new HttpUtils();
|
||||
|
||||
// Instance
|
||||
static instance: App;
|
||||
|
||||
// Static page
|
||||
staticPage: string = '';
|
||||
|
||||
// Dark mode
|
||||
darkMode: boolean = this.$cookies.isKey('dark');
|
||||
|
||||
// Show rating
|
||||
showRating: boolean = this.$cookies.get('show-rating') == 'set=yes';
|
||||
|
||||
// Demo mode
|
||||
demoMode: boolean = window.location.hostname == 'demo.vera.hydev.org' || this.$cookies.isKey('demo-mode')
|
||||
|
||||
// Is the login panel shown
|
||||
showLogin: boolean = !this.demoMode
|
||||
|
||||
/**
|
||||
* This is called when the instance is created.
|
||||
*/
|
||||
created()
|
||||
{
|
||||
// Show splash
|
||||
console.log(Constants.SPLASH);
|
||||
|
||||
// Update instance
|
||||
App.instance = this;
|
||||
|
||||
// Check location
|
||||
if (window.location.hash == '#info')
|
||||
{
|
||||
this.staticPage = 'info';
|
||||
}
|
||||
|
||||
// Default config
|
||||
if (!this.$cookies.isKey('show-rating'))
|
||||
{
|
||||
this.showRating = Constants.CURRENT_TERM == 3;
|
||||
}
|
||||
|
||||
// Demo
|
||||
if (this.demoMode)
|
||||
{
|
||||
AppDemo.loadDemo(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is called when the user logs in.
|
||||
*
|
||||
* @param user Authorization user
|
||||
*/
|
||||
onLogin(user: LoginUser)
|
||||
{
|
||||
// Hide login bar
|
||||
this.showLogin = false;
|
||||
|
||||
// Show loading message
|
||||
this.logLoading('1. Logging in...');
|
||||
|
||||
// Store user
|
||||
this.user = user;
|
||||
this.courses = user.courses
|
||||
|
||||
// Assign user to http client
|
||||
App.http.user = user;
|
||||
|
||||
// Load assignments
|
||||
this.loadAssignments();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the assignments of the courses
|
||||
*/
|
||||
loadAssignments()
|
||||
{
|
||||
// Show loading message
|
||||
this.logLoading('1. Loading assignments...');
|
||||
|
||||
// Get assignments for all the courses
|
||||
this.courses.forEach(course =>
|
||||
{
|
||||
// Send request to get assignments
|
||||
App.http.post('/assignments', {'assignmentsId': course.assignmentsId}).then(response =>
|
||||
{
|
||||
// Check success
|
||||
if (response.success)
|
||||
{
|
||||
course.loadAssignments(response.data);
|
||||
}
|
||||
else throw new Error(response.data);
|
||||
})
|
||||
.catch(e => this.showError(`Error: Assignments data failed to load.\n(${e})`));
|
||||
});
|
||||
|
||||
// Wait for assignments to be ready.
|
||||
pWaitFor(() => this.courses.every(c => c.rawAssignments != null)).then(() =>
|
||||
{
|
||||
// Filter courses
|
||||
this.gradedCourses = this.courses.filter(c => c.isGraded);
|
||||
|
||||
// Check grading algorithms
|
||||
this.checkGradingAlgorithms();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the courses' grading algorithms. (Total-mean or percent-type)
|
||||
*/
|
||||
checkGradingAlgorithms()
|
||||
{
|
||||
// Show loading message
|
||||
this.logLoading('2. Checking grading algorithms...');
|
||||
|
||||
// Loop through all the courses
|
||||
for (const course of this.gradedCourses)
|
||||
{
|
||||
for (const i of [0, 1, 2, 3])
|
||||
{
|
||||
const cookieIndex = `va.grading.${i}.${course.assignmentsId}`;
|
||||
|
||||
// Check if already exist in cookies
|
||||
if (this.$cookies.isKey(cookieIndex))
|
||||
{
|
||||
course.termGrading[i] = {method: 'TOTAL_MEAN', weightingMap: {}};
|
||||
continue;
|
||||
}
|
||||
|
||||
// Request grading scheme for this course at this grading period
|
||||
App.http.post('/grading/term', {assignmentsId: course.assignmentsId, term: i}).then(resp =>
|
||||
{
|
||||
// Check success
|
||||
if (resp.success)
|
||||
{
|
||||
// Add it to course
|
||||
course.termGrading[i] = resp.data;
|
||||
|
||||
// If it's total_mean, cache it to cookies
|
||||
// This is because only percent_type can update over time
|
||||
if (course.termGrading[i].method == 'TOTAL_MEAN')
|
||||
{
|
||||
this.$cookies.set(cookieIndex, 'TOTAL_MEAN', '3d');
|
||||
}
|
||||
}
|
||||
else throw new Error(resp.data);
|
||||
})
|
||||
.catch(e => this.showError(`Error: Grading data failed to load.\n(${e})`))
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for done
|
||||
pWaitFor(() => this.gradedCourses.every(c => c.termGrading.every(g => g != null))).then(() =>
|
||||
{
|
||||
this.assignmentsReady = true;
|
||||
|
||||
// Remove loading
|
||||
this.logLoading('');
|
||||
|
||||
// Check if rating notification should be displayed
|
||||
if (this.courses.filter(c => c.rated).length == 0 && this.showRating &&
|
||||
!this.$cookies.isKey('rating-notified'))
|
||||
{
|
||||
// Show notification
|
||||
this.$cookies.set('rating-notified', true);
|
||||
this.showUpdates()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
showUpdates()
|
||||
{
|
||||
this.$alert(
|
||||
'<b>TL;DR:</b><br/>' +
|
||||
'📅 Added a Course Selection tab to help you schedule for next year!<br/>' +
|
||||
'🤩 You can now give star ratings to your courses!<br/>' +
|
||||
'😮 You can also see others\' ratings in the course selection tab!<br/>' +
|
||||
'<br/>' +
|
||||
'That\'s it, try things out and have fun! 😇<br/>' +
|
||||
'<br/>' +
|
||||
'-- The Veracross Analyzer Team<br/>' +
|
||||
'-- Made with 🧡 in SJP',
|
||||
'🥳 Huge updates!',
|
||||
{dangerouslyUseHTMLString: true, confirmButtonText: 'OK', customClass: 'comic'});
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a message to loading screen
|
||||
*
|
||||
* @param message Message
|
||||
*/
|
||||
logLoading(message: string)
|
||||
{
|
||||
if (message == '') this.loading = '';
|
||||
else this.loading += '\n' + message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show error message on loading screen
|
||||
*
|
||||
* @param message Error message
|
||||
*/
|
||||
showError(message: string)
|
||||
{
|
||||
this.loadingError = true;
|
||||
this.loading = message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign out
|
||||
*/
|
||||
signOut()
|
||||
{
|
||||
// Clear all cookies
|
||||
this.$cookies.keys().forEach(key => this.$cookies.remove(key));
|
||||
|
||||
// Refresh
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* Select time (Eg. Term 1, Term 2, All Year, etc.)
|
||||
*
|
||||
* @param code
|
||||
*/
|
||||
selectTime(code: number)
|
||||
{
|
||||
// TODO: Optimize
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
<template>
|
||||
<div id="app" class="theme-default">
|
||||
<div id="app-inner" v-if="staticPage === ''" :class="{dark: darkMode, padding: nav.id !== 'course-selection'}">
|
||||
<login v-if="showLogin" v-on:login:user="onLogin"/>
|
||||
<navigation v-if="user != null"
|
||||
:app="this" :user="user" :nav="nav"
|
||||
@sign-out="signOut" @select-time="selectTime">
|
||||
</navigation>
|
||||
|
||||
<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>
|
||||
<course-selection v-if="nav.id === 'course-selection' && !demoMode" :app="this"></course-selection>
|
||||
<div id="demo-not-available" class="unselectable" v-if="nav.id === 'course-selection' && demoMode">
|
||||
Course selection page is not available in demo mode.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<loading v-if="loading !== ''" :text="loading" :error="loadingError"/>
|
||||
</div>
|
||||
|
||||
<Info v-if="staticPage === 'info'"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./app.ts" lang="ts"></script>
|
||||
<style src="./app.scss" lang="scss"/>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.padding
|
||||
{
|
||||
padding-bottom: 100px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,29 +0,0 @@
|
||||
<template>
|
||||
<div class="el-loading-spinner" :class="{'not-centered': !centered}">
|
||||
<svg viewBox="25 25 50 50" class="circular" :style="{width: size + 'px', height: size + 'px'}">
|
||||
<circle cx="50" cy="50" r="20" fill="none" class="path"/>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator'
|
||||
|
||||
@Component
|
||||
export default class LoadingSpinner extends Vue
|
||||
{
|
||||
@Prop({default: '42'}) size: string
|
||||
@Prop({default: true}) centered: string
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.not-centered
|
||||
{
|
||||
top: unset;
|
||||
margin-top: unset;
|
||||
width: unset;
|
||||
text-align: unset;
|
||||
position: unset;
|
||||
}
|
||||
</style>
|
||||
@@ -1,106 +0,0 @@
|
||||
|
||||
// Parent div for login
|
||||
#login
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
// Logo image
|
||||
#login-logo-image
|
||||
{
|
||||
width: 80%;
|
||||
margin-bottom: -15px;
|
||||
}
|
||||
|
||||
// Parent overlay
|
||||
.login-overlay
|
||||
{
|
||||
// Credit to w3schools.com:
|
||||
// https://www.w3schools.com/howto/howto_js_fullscreen_overlay.asp
|
||||
|
||||
// Fill entire screen
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
|
||||
// Stay in place
|
||||
position: fixed;
|
||||
|
||||
// Sit on top layer
|
||||
z-index: 1;
|
||||
|
||||
// Overlay color
|
||||
background-color: rgba(0,0,0, 0.65);
|
||||
|
||||
// Disable horizontal scroll
|
||||
overflow-x: hidden;
|
||||
|
||||
// Make it a table for vertical centering
|
||||
display: table;
|
||||
}
|
||||
|
||||
.login-vertical-center
|
||||
{
|
||||
// Vertically center
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
// The user interacting panel
|
||||
.login-panel
|
||||
{
|
||||
// Make it smaller
|
||||
width: 256px;
|
||||
|
||||
// Center
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
// Borders
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
// box-shadow: 0 0 20px 0 white;
|
||||
border: 1px solid #DCDFE6;
|
||||
|
||||
// Make it white
|
||||
background-color: white;
|
||||
|
||||
// Input bars
|
||||
.el-input
|
||||
{
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
// Button
|
||||
.el-button
|
||||
{
|
||||
margin: 5px 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// Error
|
||||
.input-error
|
||||
{
|
||||
.el-input__inner
|
||||
{
|
||||
color: #ff3a3a6b !important;
|
||||
border-color: #ff3a3a6b !important;
|
||||
background-color: #ffdddd3b !important;
|
||||
}
|
||||
|
||||
.el-input__inner:focus
|
||||
{
|
||||
background-color: white !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Fix error message
|
||||
.el-form-item__error.custom
|
||||
{
|
||||
padding-top: 0;
|
||||
position: relative;
|
||||
top: auto;
|
||||
float: left;
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
import {Component, Vue} from 'vue-property-decorator';
|
||||
import Constants from '@/constants';
|
||||
import App from '@/components/app/app';
|
||||
import VersionUtils from '@/logic/utils/version-utils';
|
||||
import LoginUser from '@/logic/login-user';
|
||||
import Maintenance from '@/components/overlays/maintenance.vue';
|
||||
|
||||
/**
|
||||
* This component handles user login, and obtains data from the server.
|
||||
*/
|
||||
@Component({components: {Maintenance}})
|
||||
export default class Login extends Vue
|
||||
{
|
||||
username = '';
|
||||
password = '';
|
||||
|
||||
loading = false;
|
||||
error = '';
|
||||
|
||||
disableInput = false;
|
||||
|
||||
maintenance = '';
|
||||
|
||||
/**
|
||||
* This is called when the instance is created.
|
||||
*/
|
||||
created()
|
||||
{
|
||||
// TODO: Check maintenance
|
||||
|
||||
// Check login cookies
|
||||
if (this.$cookies.isKey('va.token'))
|
||||
{
|
||||
// Check cookies version
|
||||
if (this.needToUpdateCookies()) this.clearCookies();
|
||||
else
|
||||
{
|
||||
// Login with token
|
||||
this.login('/login/token', {token: this.$cookies.get('va.token')});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check version number
|
||||
*
|
||||
* @returns boolean Need to clear cookies or not
|
||||
*/
|
||||
needToUpdateCookies(): boolean
|
||||
{
|
||||
// Version doesn't exist
|
||||
if (!this.$cookies.isKey('va.version')) return true;
|
||||
|
||||
// Bug
|
||||
if (this.$cookies.get('va.token') == 'undefined') return true
|
||||
|
||||
// If the current version is less than the min supported version
|
||||
return VersionUtils.compare(this.$cookies.get('va.version'), Constants.MIN_SUPPORTED_VERSION) == -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* When the user clicks, post the login request and process the response
|
||||
* This is also called when the user hits enter on the input boxes.
|
||||
*/
|
||||
loginClick()
|
||||
{
|
||||
// Simple checks
|
||||
if (this.username == '')
|
||||
{
|
||||
this.error = 'Username cannot be blank 🤔';
|
||||
}
|
||||
|
||||
// Format it
|
||||
this.username = this.username.toLowerCase().replace(/ /g, '').replace(/@.*/g, '');
|
||||
|
||||
// Actually login
|
||||
this.login('/login', {username: this.username, password: this.password})
|
||||
}
|
||||
|
||||
/**
|
||||
* Actually post the request and process the response
|
||||
*/
|
||||
login(url: string, data: any)
|
||||
{
|
||||
// Show loading
|
||||
this.disableInput = this.loading = true;
|
||||
|
||||
// Fetch request
|
||||
App.http.post(url, data).then(response =>
|
||||
{
|
||||
// Check success
|
||||
if (response.success)
|
||||
{
|
||||
// Save token to cookies
|
||||
this.$cookies.set('va.token', response.data.token, '27d');
|
||||
this.$cookies.set('va.version', Constants.VERSION, '27d');
|
||||
|
||||
// Call a custom event with the token
|
||||
this.$emit('login:user', new LoginUser(response.data));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Login expired -> clear cookies
|
||||
if (response.data == 'Error: Login expired')
|
||||
{
|
||||
this.clearCookies();
|
||||
}
|
||||
|
||||
// Show error message & allow user to retry
|
||||
// TODO: Automatic report error
|
||||
this.error = response.data;
|
||||
this.disableInput = this.loading = false;
|
||||
}
|
||||
})
|
||||
.catch(err =>
|
||||
{
|
||||
// Show error message & allow user to retry
|
||||
this.error = err;
|
||||
this.disableInput = this.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cookies
|
||||
*/
|
||||
clearCookies()
|
||||
{
|
||||
this.$cookies.keys().forEach(key => this.$cookies.remove(key));
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
<template>
|
||||
<div id="login" class="login-overlay">
|
||||
<div class="login-vertical-center">
|
||||
<div class="login-panel">
|
||||
<img id="login-logo-image" alt="logo" src="../../assets/logo.png">
|
||||
|
||||
<h1>Veracross Analyzer</h1>
|
||||
<form id="login-form">
|
||||
<el-input v-model="username"
|
||||
placeholder="SJP Username (Eg. flast21)"
|
||||
:class="{'input-error': error !== ''}"
|
||||
v-if="!disableInput"
|
||||
@keyup.enter.native="loginClick">
|
||||
</el-input>
|
||||
|
||||
<el-input v-model="password"
|
||||
placeholder="SJP Password"
|
||||
show-password=""
|
||||
:class="{'input-error': error !== ''}"
|
||||
v-if="!disableInput"
|
||||
@keyup.enter.native="loginClick">
|
||||
</el-input>
|
||||
|
||||
<div class="el-form-item__error custom">{{error}}</div>
|
||||
|
||||
<el-button plain type="primary" @click="loginClick" :loading="loading">Login</el-button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Maintenance v-if="maintenance" :message="maintenance"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./login.ts" lang="ts"></script>
|
||||
<style src="./login.scss" lang="scss"/>
|
||||
@@ -1,154 +0,0 @@
|
||||
|
||||
.el-menu.centered li
|
||||
{
|
||||
display: inline-block !important;
|
||||
float: none !important;
|
||||
}
|
||||
|
||||
// Borders
|
||||
#navigation
|
||||
{
|
||||
box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
|
||||
|
||||
ul
|
||||
{
|
||||
border-bottom-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
#nav-avatar
|
||||
{
|
||||
position: absolute;
|
||||
right: 0;
|
||||
margin: 10px 20px;
|
||||
}
|
||||
|
||||
#sign-out-button
|
||||
{
|
||||
// Float right
|
||||
position: absolute;
|
||||
right: 0;
|
||||
|
||||
// Set width and height
|
||||
height: 60px;
|
||||
width: 110px;
|
||||
}
|
||||
|
||||
#nav-grading-period
|
||||
{
|
||||
// Float right
|
||||
position: absolute;
|
||||
right: 80px;
|
||||
|
||||
// Margins
|
||||
margin-top: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
#nav-title
|
||||
{
|
||||
// Float left
|
||||
position: absolute;
|
||||
left: 0;
|
||||
|
||||
// Set height
|
||||
height: 60px;
|
||||
|
||||
// Center text
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
// Margins
|
||||
margin-left: 20px;
|
||||
margin-right: 8px;
|
||||
|
||||
// Make it non-clickable
|
||||
pointer-events: none;
|
||||
|
||||
#nav-logo
|
||||
{
|
||||
height: 70%;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
#nav-logo-text
|
||||
{
|
||||
// Color
|
||||
color: #6bbeff !important;
|
||||
background: linear-gradient(90deg,
|
||||
rgba(90,177,239,1) 0%,
|
||||
rgba(25,212,174,1) 100%) !important;
|
||||
|
||||
// Font
|
||||
font-weight: 500;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
#nav-logo-text.logo-text
|
||||
{
|
||||
// Override the background
|
||||
-webkit-text-fill-color: transparent !important;
|
||||
-webkit-background-clip: text !important;
|
||||
}
|
||||
|
||||
#nav-logo-version
|
||||
{
|
||||
color: #a7a7a7;
|
||||
margin-left: 5px;
|
||||
margin-top: 8px;
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
#next-course
|
||||
{
|
||||
// Down center
|
||||
width: 50%;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 25%;
|
||||
padding-top: 2px;
|
||||
box-shadow: 0 -2px 9px 0 #00000029;
|
||||
}
|
||||
|
||||
footer
|
||||
{
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#prev-course
|
||||
{
|
||||
// Up center
|
||||
width: 50%;
|
||||
position: absolute;
|
||||
top: 61px;
|
||||
left: 25%;
|
||||
padding-bottom: 2px;
|
||||
box-shadow: 0 2px 9px 0 #00000029;
|
||||
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
.nav-course-operations
|
||||
{
|
||||
// Background
|
||||
background-color: rgba(214, 214, 214, 0.67);
|
||||
opacity: 0.85;
|
||||
|
||||
// Font
|
||||
font-size: 14px;
|
||||
color: #ab8585;
|
||||
|
||||
// Cursor
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.el-submenu__title
|
||||
{
|
||||
padding-right: 5px !important;
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
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.ts';
|
||||
|
||||
/**
|
||||
* This component is the top navigation bar
|
||||
*/
|
||||
@Component
|
||||
export default class Navigation extends Vue
|
||||
{
|
||||
@Prop({required: true}) app: App;
|
||||
@Prop({required: true}) nav: NavController;
|
||||
@Prop({required: true}) user: LoginUser;
|
||||
|
||||
private gradingPeriod: string = 'All Year';
|
||||
|
||||
// Instance
|
||||
static instance: Navigation;
|
||||
|
||||
/**
|
||||
* This is called when the instance is created.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
mounted()
|
||||
{
|
||||
Navigation.instance = this;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is called when the selection changes.
|
||||
*
|
||||
* @param index The index selected
|
||||
* @param indexPath The path of the index
|
||||
*/
|
||||
onSelect(index: string, indexPath: string)
|
||||
{
|
||||
// Update active index
|
||||
try
|
||||
{
|
||||
// Is json
|
||||
this.nav.updateIndex(JSON.parse(index))
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
// Not json
|
||||
this.nav.updateIndex(index);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move to the next course
|
||||
*
|
||||
* @param indexOffset Index offset (Eg. 1 for next)
|
||||
*/
|
||||
nextCourse(indexOffset: number)
|
||||
{
|
||||
// Set tab to the next index
|
||||
this.nav.updateIndex(this.findNextCourse(indexOffset).urlIndex)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the next course
|
||||
*
|
||||
* @param indexOffset Index offset (Eg. 1 for next)
|
||||
*/
|
||||
findNextCourse(indexOffset: number)
|
||||
{
|
||||
return this.findCourse(this.nav.info.id, indexOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find course
|
||||
*
|
||||
* @param courseId Course ID
|
||||
* @param indexOffset Index offset (Eg. 1 for next)
|
||||
*/
|
||||
findCourse(courseId: string, indexOffset: number)
|
||||
{
|
||||
// Find current course index
|
||||
let courseIndex = this.app.gradedCourses.findIndex(c => c.id == +courseId);
|
||||
|
||||
// Find next course
|
||||
return this.app.gradedCourses[courseIndex + indexOffset];
|
||||
}
|
||||
|
||||
/**
|
||||
* Select grading period
|
||||
*
|
||||
* @param command Term 1, Term 2, All Year, etc.
|
||||
*/
|
||||
selectGradingPeriod(command: string)
|
||||
{
|
||||
this.gradingPeriod = command;
|
||||
this.$cookies.set('va.grading-period', command, '10y');
|
||||
|
||||
// Call event
|
||||
this.$emit('select-time', this.getSelectedTerm());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get code for selected time
|
||||
*/
|
||||
getSelectedTerm(): number
|
||||
{
|
||||
if (this.gradingPeriod == 'All Year') return -1;
|
||||
else return +this.gradingPeriod.replace('Term ', '') - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Avatar dropdown menu event
|
||||
*
|
||||
* @param cmd Command: sign-out
|
||||
*/
|
||||
onAvatarMenu(cmd: string)
|
||||
{
|
||||
switch (cmd)
|
||||
{
|
||||
case 'sign-out':
|
||||
{
|
||||
this.$emit('sign-out');
|
||||
break
|
||||
}
|
||||
case 'switch-dark':
|
||||
{
|
||||
this.app.darkMode = !this.app.darkMode;
|
||||
|
||||
if (this.app.darkMode) this.$cookies.set('dark', true);
|
||||
else this.$cookies.remove('dark');
|
||||
|
||||
break
|
||||
}
|
||||
case 'switch-rating':
|
||||
{
|
||||
this.app.showRating = !this.app.showRating;
|
||||
|
||||
if (this.app.showRating) this.$cookies.set('show-rating', 'set=yes', '30d');
|
||||
else this.$cookies.set('show-rating', 'set=no', '30d');
|
||||
|
||||
break
|
||||
}
|
||||
case 'updates':
|
||||
{
|
||||
this.app.showUpdates()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get version() {return Constants.VERSION}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
<template>
|
||||
<div id="navigation">
|
||||
<el-menu style="margin-bottom: 10px;" class="centered" mode="horizontal"
|
||||
:default-active="nav.id" @select="onSelect">
|
||||
|
||||
<div id="nav-title">
|
||||
<img id="nav-logo" alt="logo" src="../../assets/logo.png">
|
||||
<span id="nav-logo-text" class="logo-text">Veracross Analyzer</span>
|
||||
<span id="nav-logo-version">v{{version}}</span>
|
||||
</div>
|
||||
|
||||
<el-menu-item index="overall">Overall</el-menu-item>
|
||||
|
||||
<el-submenu index="">
|
||||
<template slot="title">Courses</template>
|
||||
<el-menu-item v-for="course in app.gradedCourses"
|
||||
:index="JSON.stringify(course.urlIndex)"
|
||||
:key="course.id">{{course.name}}</el-menu-item>
|
||||
</el-submenu>
|
||||
|
||||
<el-menu-item index="course-selection">Course Selection</el-menu-item>
|
||||
|
||||
<!-- Grading period selection -->
|
||||
<el-dropdown id="nav-grading-period" @command="selectGradingPeriod">
|
||||
<el-button type="primary" size="medium">
|
||||
{{gradingPeriod}}<i class="el-icon-arrow-down el-icon--right"/>
|
||||
</el-button>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item command="Term 1">Term 1</el-dropdown-item>
|
||||
<el-dropdown-item command="Term 2">Term 2</el-dropdown-item>
|
||||
<el-dropdown-item command="Term 3">Term 3</el-dropdown-item>
|
||||
<el-dropdown-item command="Term 4">Term 4</el-dropdown-item>
|
||||
<!-- TODO: Auto enable / disable quarters -->
|
||||
<el-dropdown-item command="All Year" divided>All Year</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
|
||||
<!-- User avatar -->
|
||||
<el-dropdown id="nav-avatar" trigger="click" @command="onAvatarMenu">
|
||||
<el-avatar :src="user.avatarUrl"/>
|
||||
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item style="text-align: center">{{user.firstName}}</el-dropdown-item>
|
||||
|
||||
<el-dropdown-item icon="el-icon-sunrise" command="switch-dark" divided>
|
||||
{{!app.darkMode ? 'Dark Mode (Unfinished)' : 'Light Mode'}}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item icon="el-icon-edit-outline" command="switch-rating">
|
||||
{{app.showRating ? 'Hide rating buttons' : 'Show rating button'}}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item icon="el-icon-cold-drink" command="updates">
|
||||
Check out the updates
|
||||
</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="nav.id === 'course' && findNextCourse(-1) != null"
|
||||
@click="nextCourse(-1)" id="prev-course" class="nav-course-operations unselectable">
|
||||
▲ PREVIOUS COURSE ▲
|
||||
</div>
|
||||
<footer>
|
||||
<div v-if="nav.id === 'course' && findNextCourse(1) != null"
|
||||
@click="nextCourse(1)" id="next-course" class="nav-course-operations unselectable">
|
||||
▼ NEXT COURSE ▼
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Back to top -->
|
||||
<el-backtop style="box-shadow: rgba(0, 0, 0, 0.23) 0 3px 11px 0;"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./navigation.ts" lang="ts"></script>
|
||||
<style src="./navigation.scss" lang="scss"/>
|
||||
@@ -1,112 +0,0 @@
|
||||
<template>
|
||||
<div id="loading">
|
||||
<div id="text" :class="message">
|
||||
{{message}}
|
||||
|
||||
<div v-if="!error" class="el-loading-spinner">
|
||||
<svg viewBox="25 25 50 50" class="circular">
|
||||
<circle cx="50" cy="50" r="20" fill="none" class="path" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div v-if="error" id="error-details">
|
||||
<span v-for="(line, index) in split" :style="`font-size: ${-index === 0 ? 16 : 12}px;`">
|
||||
{{line}}
|
||||
<br>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!error" id="details">
|
||||
<span v-for="(line, index) in split" :style="`font-size: ${16 - split.length + index}px;`">
|
||||
{{line}}
|
||||
<br>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
|
||||
@Component
|
||||
export default class Loading extends Vue
|
||||
{
|
||||
@Prop({required: true}) text: string;
|
||||
|
||||
@Prop({required: true}) error: boolean;
|
||||
|
||||
get split()
|
||||
{
|
||||
return this.text.split('\n');
|
||||
}
|
||||
|
||||
get message()
|
||||
{
|
||||
return this.error ? 'Error' : 'Loading';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#loading
|
||||
{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
box-shadow: inset 0 0 1px 1px rgba(0,0,0,.1);
|
||||
background: -webkit-linear-gradient(left, rgba(95, 18, 72, 0.4), rgba(42, 81, 117, 0.4) 100%);
|
||||
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.Error
|
||||
{
|
||||
color: #ffdddd !important;
|
||||
}
|
||||
|
||||
#text
|
||||
{
|
||||
color: white;
|
||||
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
font-size: 46px;
|
||||
}
|
||||
|
||||
#details
|
||||
{
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
margin-top: -5px;
|
||||
font-size: 16px;
|
||||
color: #f9f9f9;
|
||||
}
|
||||
|
||||
#error-details
|
||||
{
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.el-loading-spinner
|
||||
{
|
||||
top: unset !important;
|
||||
margin-top: 0 !important;
|
||||
width: unset !important;
|
||||
position: unset !important;
|
||||
}
|
||||
|
||||
.el-loading-spinner .path
|
||||
{
|
||||
stroke: white;
|
||||
}
|
||||
</style>
|
||||
@@ -1,63 +0,0 @@
|
||||
<template>
|
||||
<div id="maintenance">
|
||||
<div id="maintenance-content">
|
||||
<h1>We’ll be back soon!</h1>
|
||||
<div>
|
||||
<p>Sorry for the inconvenience but we’re performing some maintenance at the moment.
|
||||
We’ll be back online shortly!</p>
|
||||
|
||||
<p>What went wrong: {{json.reason}}</p>
|
||||
|
||||
<p>Estimated fix: {{json.eta}}</p>
|
||||
|
||||
<p>— An Average SJP Junior</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
|
||||
@Component
|
||||
export default class Maintenance extends Vue
|
||||
{
|
||||
@Prop({required: true}) message: any;
|
||||
|
||||
get json()
|
||||
{
|
||||
return JSON.parse(this.message);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
#maintenance
|
||||
{
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
#maintenance-content
|
||||
{
|
||||
font: 20px Helvetica, sans-serif;
|
||||
color: #333;
|
||||
|
||||
display: block;
|
||||
text-align: left;
|
||||
margin: 150px;
|
||||
|
||||
h1
|
||||
{
|
||||
font-size: 50px;
|
||||
}
|
||||
|
||||
a {color: #dc8100; text-decoration: none;}
|
||||
a:hover {color: #333; text-decoration: none;}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
Before Width: | Height: | Size: 2.0 KiB |
@@ -1,36 +0,0 @@
|
||||
<template>
|
||||
<div class="background">
|
||||
<span :style="{width: (score / 5 * 100).toFixed(2) + '%'}" class="rating"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator'
|
||||
|
||||
@Component
|
||||
export default class StarRating extends Vue
|
||||
{
|
||||
@Prop({required: true}) score: number;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.background
|
||||
{
|
||||
background: url("./star-rating-sprite.png") repeat-x;
|
||||
font-size: 0;
|
||||
height: 21px;
|
||||
line-height: 0;
|
||||
overflow: hidden;
|
||||
width: 110px;
|
||||
margin: 0 auto;
|
||||
|
||||
span.rating
|
||||
{
|
||||
background: url("./star-rating-sprite.png") repeat-x 0 100%;
|
||||
float: left;
|
||||
height: 21px;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,81 +0,0 @@
|
||||
/**
|
||||
* This class stores the static constants.
|
||||
*/
|
||||
import {findLastIndex} from '@/logic/utils/general-utils';
|
||||
|
||||
export default class Constants
|
||||
{
|
||||
/** Base url for api access */
|
||||
static API_URL: string = 'https://va.hydev.org/api';
|
||||
// static API_URL: string = 'http://localhost:24021/api';
|
||||
|
||||
/** Current version */
|
||||
static VERSION: string = '0.5.6.1761';
|
||||
|
||||
/** The minimum version that still supports the same cookies */
|
||||
static MIN_SUPPORTED_VERSION: string = '0.4.6.1087';
|
||||
|
||||
static GITHUB: string = 'https://github.com/HyDevelop/VeracrossAnalyzer.Client';
|
||||
|
||||
static SPLASH: string =
|
||||
'. , ,---. | \n' +
|
||||
'| |. , |---|,---.,---.| , .,---,,---.,---.\n' +
|
||||
' \\ / >< | || |,---|| | | .-\' |---\'| \n' +
|
||||
' `\' \' ` ` \'` \'`---^`---\'`---|\'---\'`---\'` \n' +
|
||||
' `---\' \n' +
|
||||
` Version v${Constants.VERSION} by Hykilpikonna (YGui21)\n` +
|
||||
` Github: ${Constants.GITHUB}`;
|
||||
|
||||
// Graph Theme
|
||||
static THEME =
|
||||
{
|
||||
// Colors
|
||||
colors:
|
||||
[
|
||||
'#19d4ae',
|
||||
'#5ab1ef',
|
||||
'#fa6e86',
|
||||
'#ffb980',
|
||||
'#0067a6',
|
||||
'#c4b4e4',
|
||||
'#d87a80',
|
||||
'#9cbbff',
|
||||
'#d9d0c7',
|
||||
'#87a997',
|
||||
'#d49ea2',
|
||||
'#5b4947',
|
||||
'#7ba3a8',
|
||||
'#fc97af',
|
||||
'#919e8b',
|
||||
'#d7ab82',
|
||||
'#6e7074',
|
||||
'#61a0a8',
|
||||
'#efa18d',
|
||||
'#787464',
|
||||
'#cc7e63',
|
||||
'#724e58',
|
||||
'#4b565b'
|
||||
]
|
||||
};
|
||||
|
||||
// Terms (TODO: Actually get the terms dynamically
|
||||
static TERMS =
|
||||
[
|
||||
new Date('Sep 04 2019'),
|
||||
new Date('Nov 03 2019'),
|
||||
new Date('Jan 19 2020'),
|
||||
new Date('Mar 22 2020'),
|
||||
new Date('Jun 05 2020'),
|
||||
];
|
||||
static CURRENT_TERM = Constants.getTerm(new Date());
|
||||
|
||||
/**
|
||||
* Find out the specified date is in which term
|
||||
*
|
||||
* @param date
|
||||
*/
|
||||
static getTerm(date: Date)
|
||||
{
|
||||
return findLastIndex(Constants.TERMS, d => d <= date);
|
||||
}
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
import {CourseUtils} from '@/logic/utils/course-utils';
|
||||
|
||||
export default class CourseInfo
|
||||
{
|
||||
id_ci: number
|
||||
year: number
|
||||
name: string
|
||||
teacher: string
|
||||
level: string
|
||||
courseIds: number[]
|
||||
|
||||
uniqueName: string
|
||||
courseCount: number
|
||||
gradeLevels: number[]
|
||||
enrollments: number
|
||||
classes: ClassInfo[]
|
||||
levelID: number;
|
||||
levelFull: string
|
||||
|
||||
rating: AnalyzedRating = null as any as AnalyzedRating
|
||||
|
||||
/**
|
||||
* Construct with a json object
|
||||
*
|
||||
* @param json
|
||||
*/
|
||||
constructor(json: any)
|
||||
{
|
||||
this.id_ci = json.id_ci
|
||||
this.year = json.year
|
||||
this.name = json.name.trim().replace('&', '&').replace('"', '"')
|
||||
this.teacher = json.teacher
|
||||
this.level = json.level
|
||||
this.courseIds = json.courseIds.split('|').map((id: string) => +id);
|
||||
|
||||
this.courseCount = this.courseIds.length;
|
||||
this.gradeLevels = [];
|
||||
this.uniqueName = CourseInfo.toUniqueName(this.name);
|
||||
this.enrollments = 0;
|
||||
this.classes = []
|
||||
this.levelID = CourseUtils.getLevelID(this.level);
|
||||
this.levelFull = CourseUtils.getLevelFullName(this.level);
|
||||
}
|
||||
|
||||
static toUniqueName(name: string)
|
||||
{
|
||||
return name
|
||||
.replace(/( Semester| Full Year|)/g, '')
|
||||
.replace(/( Accelerated| Honors| College Prep|)/g, '')
|
||||
.replace(/( A| Acc| CP| H| \(.*\))$/g, '');
|
||||
}
|
||||
}
|
||||
|
||||
export class UniqueCourse
|
||||
{
|
||||
name: string
|
||||
courses: CourseInfo[]
|
||||
enrollments: number
|
||||
|
||||
constructor(name: string, courses: CourseInfo[], enrollments: number)
|
||||
{
|
||||
this.name = name;
|
||||
this.courses = courses;
|
||||
this.enrollments = enrollments;
|
||||
}
|
||||
|
||||
get classes()
|
||||
{
|
||||
return this.courses.flatMap(c => c.classes);
|
||||
}
|
||||
}
|
||||
|
||||
export class ClassInfo
|
||||
{
|
||||
id: number
|
||||
name: string
|
||||
teacher: string
|
||||
level: string
|
||||
|
||||
uniqueName: string
|
||||
|
||||
/**
|
||||
* Construct with a json object
|
||||
*
|
||||
* @param json
|
||||
*/
|
||||
constructor(json: any)
|
||||
{
|
||||
this.id = json.id;
|
||||
this.name = json.name.trim().replace('&', '&').replace('"', '"')
|
||||
this.teacher = json.teacher
|
||||
this.level = json.level;
|
||||
|
||||
this.uniqueName = CourseInfo.toUniqueName(this.name);
|
||||
}
|
||||
}
|
||||
|
||||
export class CourseInfoRating
|
||||
{
|
||||
id_ci: number
|
||||
id_user: number
|
||||
firstName: string
|
||||
lastName: string
|
||||
anonymous: boolean
|
||||
ratings: number[]
|
||||
comment: string
|
||||
|
||||
averageRating: number = 0
|
||||
|
||||
constructor(json: any)
|
||||
{
|
||||
this.id_ci = json.id_ci;
|
||||
this.id_user = json.id_user;
|
||||
this.anonymous = this.id_user == -1;
|
||||
this.ratings = json.ratings;
|
||||
this.comment = json.comment;
|
||||
|
||||
if (json.userFullName != null)
|
||||
{
|
||||
let nameSplit = json.userFullName.split(']=[');
|
||||
this.firstName = nameSplit[0];
|
||||
this.lastName = nameSplit[1];
|
||||
}
|
||||
|
||||
this.ratings.forEach(r => this.averageRating += r);
|
||||
this.averageRating /= 5;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new for posting to the server
|
||||
* @param id_ci
|
||||
*/
|
||||
public static createNew(id_ci: number)
|
||||
{
|
||||
return new CourseInfoRating({id_ci: id_ci, id_user: -2, userFullName: null,
|
||||
anonymous: false, ratings: [0,0,0,0,0], comment: ''})
|
||||
}
|
||||
}
|
||||
|
||||
export class AnalyzedRating
|
||||
{
|
||||
ratingCounts: number[][] // ratingCounts[criteria][stars] = count
|
||||
ratingSums: number[] // ratingSums[criteria] = total stars
|
||||
totalCount: number
|
||||
|
||||
ratingAverages: number[] = [] // ratingAverages[criteria] = average
|
||||
overallRating: number = 0
|
||||
|
||||
constructor(json: any)
|
||||
{
|
||||
this.ratingCounts = json.ratingCounts;
|
||||
this.ratingSums = json.ratingSums;
|
||||
this.totalCount = json.totalCount;
|
||||
|
||||
// No ratings
|
||||
if (this.totalCount == 0)
|
||||
{
|
||||
this.overallRating = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate overall rating
|
||||
this.ratingSums.forEach((criteriaScore, i) => this.overallRating += this.ratingAverages[i] = criteriaScore / this.totalCount);
|
||||
this.overallRating /= this.ratingAverages.length;
|
||||
}
|
||||
}
|
||||
|
||||
export const RATING_CRITERIA: {title: string, desc: string}[] =
|
||||
[
|
||||
{title: 'Enjoyable', desc: 'How enjoyable is the course?'},
|
||||
{title: 'Knowledge', desc: 'How interesting is the content of the course? ' +
|
||||
'Is it something you feel worth learning?'},
|
||||
{title: 'Interactivity', desc: 'How interesting is the teacher? Is the teacher interactive?'},
|
||||
{title: 'Eloquence', desc: `Are the teacher's lectures easy to understand?`},
|
||||
{title: 'Fairness', desc: `How fair is the teacher's grading? Is credit given in proportion to effort?`}
|
||||
];
|
||||
@@ -1,436 +0,0 @@
|
||||
import {FormatUtils} from '@/logic/utils/format-utils';
|
||||
import {CourseUtils} from '@/logic/utils/course-utils';
|
||||
import Navigation from '@/components/navigation/navigation';
|
||||
import {GPAUtils} from '@/logic/utils/gpa-utils';
|
||||
import CacheUtils from '@/logic/utils/cache-utils';
|
||||
import Constants from '@/constants';
|
||||
import {Index} from '@/logic/nav-controller';
|
||||
import App from '@/components/app/app';
|
||||
import {CourseInfoRating} from '@/logic/course-info';
|
||||
|
||||
/**
|
||||
* Objects of this interface represent assignment grades.
|
||||
*/
|
||||
export class Assignment
|
||||
{
|
||||
id: number;
|
||||
scoreId: number;
|
||||
type: string;
|
||||
typeId: number;
|
||||
description: string;
|
||||
time: number;
|
||||
complete: string;
|
||||
include: boolean;
|
||||
display: boolean;
|
||||
|
||||
unread: boolean;
|
||||
|
||||
scoreMax: number;
|
||||
score: number;
|
||||
|
||||
gradingPeriod: number;
|
||||
|
||||
// Callbacks when this object updates
|
||||
private updateCallbacks: (() => void)[] = [];
|
||||
|
||||
/**
|
||||
* Construct assignment with json object
|
||||
*
|
||||
* @param json Json object
|
||||
*/
|
||||
constructor(json: any)
|
||||
{
|
||||
this.id = json.assignment_id;
|
||||
this.scoreId = json.score_id;
|
||||
this.type = json.assignment_type;
|
||||
this.typeId = json.assignment_type_id;
|
||||
this.description = json.assignment_description;
|
||||
this.time = new Date(json._date).getTime();
|
||||
this.complete = json.completion_status;
|
||||
this.include = json.include_in_calculated_grade == 1;
|
||||
this.display = json.display_grade == 1;
|
||||
|
||||
this.unread = json.is_unread == 1;
|
||||
|
||||
this.scoreMax = json.maximum_score;
|
||||
this.score = +json.raw_score;
|
||||
|
||||
// 0, 1, 2, 3 contains quarter assignments, 4 contains final assignments
|
||||
if (json.grading_period.toLowerCase() == 'all') this.gradingPeriod = 4;
|
||||
else this.gradingPeriod = +json.grading_period.replace('Quarter ', '') - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Graded or not
|
||||
*/
|
||||
get graded()
|
||||
{
|
||||
// TODO: Add more cases
|
||||
// Incomplete doesn't mean that the teacher didn't grade it yet, which is "Pending".
|
||||
// NREQ is not graded.
|
||||
return this.include && (this.complete == 'Complete' || this.complete == 'Late' || this.complete == 'Incomplete' || this.complete == 'Not Turned In');
|
||||
}
|
||||
|
||||
/**
|
||||
* What is the problem with this assignment
|
||||
*
|
||||
* @return string Empty string if complete, otherwise return problem.
|
||||
*/
|
||||
get problem()
|
||||
{
|
||||
switch (this.complete)
|
||||
{
|
||||
case 'Pending': return 'Pending'; // ID: 0
|
||||
case 'Not Turned In': return 'Not Turned In'; // ID: 1
|
||||
case 'Incomplete': return 'Incomplete'; // ID: 2
|
||||
case 'Complete': return ''; // ID: 3
|
||||
case 'NREQ': return 'Dropped'; // ID: 4
|
||||
case 'Late': return 'Late';
|
||||
default: return this.complete;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the text color of the problem
|
||||
*/
|
||||
get problemColor()
|
||||
{
|
||||
switch (this.complete)
|
||||
{
|
||||
case 'Pending': return '#b1b1b1';
|
||||
case 'Not Turned In': return '#ff0036';
|
||||
case 'Incomplete': return '#ff7a2f';
|
||||
case 'NREQ': return '#41b141';
|
||||
case 'Late': return '#ff7a2f';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add callback
|
||||
*
|
||||
* @param callback
|
||||
*/
|
||||
addCallback(callback: () => void)
|
||||
{
|
||||
this.updateCallbacks.push(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark as read
|
||||
*/
|
||||
markAsRead(): Promise<void>
|
||||
{
|
||||
return new Promise((resolve, reject) => {
|
||||
App.http.post('/mark-as-read', {scoreId: this.scoreId})
|
||||
.then(response =>
|
||||
{
|
||||
// Check success
|
||||
if (response.success)
|
||||
{
|
||||
this.unread = false;
|
||||
this.updateCallbacks.forEach(callback => callback());
|
||||
resolve();
|
||||
}
|
||||
else reject(response.data);
|
||||
})
|
||||
.catch(reject)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export interface AssignmentType
|
||||
{
|
||||
id: number
|
||||
name: string
|
||||
|
||||
weight: number
|
||||
scoreMax: number
|
||||
score: number
|
||||
percent: number
|
||||
assignmentCount: number
|
||||
|
||||
graded: boolean
|
||||
}
|
||||
|
||||
export interface Grading
|
||||
{
|
||||
method: string
|
||||
weightingMap: {[index: string]: number}
|
||||
}
|
||||
|
||||
export default class Course
|
||||
{
|
||||
id: number
|
||||
id_ci: number
|
||||
assignmentsId: number
|
||||
name: string
|
||||
teacherName: string
|
||||
status: string
|
||||
rawAssignments: Assignment[]
|
||||
rating: CourseInfoRating
|
||||
rated: boolean
|
||||
|
||||
rawLetterGrade?: string
|
||||
rawNumericGrade?: number
|
||||
|
||||
level: string
|
||||
scaleUp: number
|
||||
|
||||
termGrading: Grading[]
|
||||
termAssignments: Assignment[][]
|
||||
|
||||
cache: CacheUtils = new CacheUtils();
|
||||
|
||||
/**
|
||||
* Construct a course with a course json object
|
||||
*
|
||||
* @param courseJson Course json object
|
||||
*/
|
||||
constructor(courseJson: any)
|
||||
{
|
||||
this.id = courseJson.id;
|
||||
this.id_ci = courseJson.id_ci;
|
||||
this.assignmentsId = courseJson.assignmentsId;
|
||||
this.name = FormatUtils.parseText(courseJson.name).trim();
|
||||
this.teacherName = courseJson.teacherName;
|
||||
this.status = courseJson.status;
|
||||
this.rated = courseJson.rating != null;
|
||||
this.rating = this.rated ? new CourseInfoRating(courseJson.rating) : CourseInfoRating.createNew(this.id_ci);
|
||||
|
||||
this.rawLetterGrade = courseJson.letterGrade;
|
||||
this.rawNumericGrade = courseJson.numericGrade;
|
||||
|
||||
// Other api issue
|
||||
if (this.rawLetterGrade == '')
|
||||
{
|
||||
this.rawNumericGrade = undefined;
|
||||
this.rawLetterGrade = undefined;
|
||||
}
|
||||
|
||||
// Level and scaleUp TODO: Use server course level
|
||||
let level = CourseUtils.getLevel(courseJson.level);
|
||||
this.level = level.level;
|
||||
this.scaleUp = level.scaleUp;
|
||||
|
||||
this.termGrading = new Array(4).fill(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load in assignments data
|
||||
*
|
||||
* @param data Assignments data
|
||||
*/
|
||||
loadAssignments(data: any)
|
||||
{
|
||||
// Load assignments
|
||||
// Parse json and filter it
|
||||
this.rawAssignments = data.assignments.map((a: any) => new Assignment(a));
|
||||
|
||||
// Sort by date (Latest is at 0)
|
||||
this.rawAssignments.sort((a, b) => b.time - a.time);
|
||||
|
||||
// Filter assignments into terms
|
||||
this.termAssignments = [[], [], [], [], []];
|
||||
|
||||
// Loop through it by time order
|
||||
this.rawAssignments.forEach(a => this.termAssignments[a.gradingPeriod].push(a));
|
||||
}
|
||||
|
||||
/**
|
||||
* Is graded or not
|
||||
*/
|
||||
get isGraded(): boolean
|
||||
{
|
||||
// Skip future or past courses
|
||||
if (this.status != 'active') return false;
|
||||
|
||||
// Skip courses without levels TODO: Ask for user input
|
||||
if (this.level == 'None' || this.level == 'Unknown' || this.scaleUp == -1) return false;
|
||||
|
||||
// Skip courses without graded assignments
|
||||
if (this.assignments.length == 0) return false;
|
||||
|
||||
// Skip if there are no grading scale
|
||||
// if (course.grading.method == 'NOT_GRADED') return;
|
||||
|
||||
// Is graded
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get currently selected grading periods
|
||||
*/
|
||||
get gradingPeriods(): number[]
|
||||
{
|
||||
return this.cache.get('GradingPeriods', () =>
|
||||
{
|
||||
return (this.rawSelectedTerm == -1 ? [0, 1, 2, 3] : [this.rawSelectedTerm]).filter(term =>
|
||||
this.termAssignments[term].filter(a => a.graded).length != 0);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get currently selected grading periods
|
||||
*/
|
||||
get allGradingPeriods(): number[]
|
||||
{
|
||||
return this.cache.get('AllGradingPeriods', () =>
|
||||
{
|
||||
return [0, 1, 2, 3].filter(term => this.termAssignments[term].filter(a => a.graded).length != 0);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get assignments of the selected grading periods
|
||||
*/
|
||||
get assignments(): Assignment[]
|
||||
{
|
||||
return this.gradingPeriods
|
||||
.flatMap(term => this.termAssignments[term])
|
||||
.sort((a, b) => b.time - a.time);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get assignments before a certain date
|
||||
*
|
||||
* @param time
|
||||
*/
|
||||
getAssignmentsBefore(time: number): {term: number, assignments: Assignment[]}
|
||||
{
|
||||
let term = Constants.getTerm(new Date(time));
|
||||
let assignments = this.assignments.filter(a => a.gradingPeriod == term && a.time <= time);
|
||||
|
||||
return {term: term, assignments: assignments}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get letter grade
|
||||
*/
|
||||
get letterGrade(): string
|
||||
{
|
||||
return this.cache.get('LetterGrade', () =>
|
||||
{
|
||||
// Get scale
|
||||
let scale = GPAUtils.findScale(this.numericGrade);
|
||||
|
||||
// Scale not found
|
||||
return scale == undefined ? '--' : scale.letter;
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get letter grade by term
|
||||
*
|
||||
* @param term
|
||||
*/
|
||||
letterGradeTerm(term: number): string
|
||||
{
|
||||
return this.cache.get('LetterGrade' + term, () =>
|
||||
{
|
||||
// Get scale
|
||||
let scale = GPAUtils.findScale(this.numericGradeTerm(term));
|
||||
|
||||
// Scale not found
|
||||
return scale == undefined ? '--' : scale.letter;
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get numeric grade
|
||||
*/
|
||||
get numericGrade()
|
||||
{
|
||||
return this.cache.get('NumericGrade', () =>
|
||||
{
|
||||
return this.gradingPeriods.map(term => this.numericGradeTerm(term))
|
||||
.reduce((p, v) => p + v) / this.gradingPeriods.length
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get numeric grade by term
|
||||
*
|
||||
* @param term
|
||||
*/
|
||||
numericGradeTerm(term: number): number
|
||||
{
|
||||
return this.cache.get('NumericGrade' + term, () =>
|
||||
{
|
||||
// Calculate
|
||||
if (this.termGrading[term].method == 'PERCENT_TYPE')
|
||||
{
|
||||
return GPAUtils.getPercentTypeAverage(this.termGrading[term], this.termAssignments[term]);
|
||||
}
|
||||
else if (this.termGrading[term].method == 'TOTAL_MEAN')
|
||||
{
|
||||
return GPAUtils.getTotalMeanAverage(this.termAssignments[term]);
|
||||
}
|
||||
else return -1;
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get assignment types
|
||||
*/
|
||||
get assignmentTypes(): AssignmentType[]
|
||||
{
|
||||
return this.cache.get('AssignmentTypes', () =>
|
||||
{
|
||||
// Get all types
|
||||
let types = this.assignments.map(a => a.type);
|
||||
|
||||
// Remove duplicates
|
||||
types = types.filter((type, i, a) => a.indexOf(type) == i);
|
||||
|
||||
// Get total possible score for weight calculation
|
||||
let totalScoreMax = this.assignments.reduce((sum, a) => sum + a.scoreMax, 0);
|
||||
|
||||
// For every type...
|
||||
return types.map(type =>
|
||||
{
|
||||
// Get assignments of the type
|
||||
let typeAssignments = this.assignments.filter(a => a.type == type);
|
||||
|
||||
// Get graded assignments
|
||||
let gradedAssignments = typeAssignments.filter(a => a.graded);
|
||||
|
||||
// Count scores and max scores
|
||||
let score = gradedAssignments.reduce((sum, a) => sum + a.score, 0);
|
||||
let scoreMax = gradedAssignments.reduce((sum, a) => sum + a.scoreMax, 0);
|
||||
|
||||
// Calculate weight
|
||||
let weight = this.termGrading[0].method == 'PERCENT_TYPE'
|
||||
? this.termGrading[0].weightingMap[type] : scoreMax / totalScoreMax;
|
||||
|
||||
// Return
|
||||
return {name: type, id: typeAssignments[0].typeId, weight: +(weight * 100).toFixed(2),
|
||||
scoreMax: scoreMax, score: score, percent: +(score / scoreMax * 100).toFixed(2),
|
||||
assignmentCount: typeAssignments.length, graded: gradedAssignments.length > 0}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import {GPAUtils} from '@/logic/utils/gpa-utils';
|
||||
import Course from '@/logic/course';
|
||||
|
||||
const md5 = require('md5');
|
||||
|
||||
export default class LoginUser
|
||||
{
|
||||
id: number
|
||||
schoolPersonPk: number
|
||||
username: string
|
||||
lastLogin: Date
|
||||
firstLogin: Date
|
||||
firstName: string
|
||||
lastName: string
|
||||
graduationYear: number
|
||||
emails: string[]
|
||||
classes: string[]
|
||||
avatarUrl: string
|
||||
|
||||
token: string
|
||||
courses: Course[]
|
||||
|
||||
gradeLevel: number
|
||||
gradeLevelName: string
|
||||
|
||||
constructor(jsonData: any)
|
||||
{
|
||||
let json = jsonData.user
|
||||
this.id = json.id;
|
||||
this.schoolPersonPk = json.schoolPersonPk;
|
||||
this.username = json.username;
|
||||
this.lastLogin = new Date(json.lastLogin);
|
||||
this.firstLogin = new Date(json.firstLogin);
|
||||
this.firstName = json.firstName;
|
||||
this.lastName = json.lastName;
|
||||
this.graduationYear = +json.graduationYear;
|
||||
this.emails = json.emails.split('|').map((e: any) => e.toLowerCase().trim());
|
||||
this.classes = json.classes.split('|');
|
||||
this.avatarUrl = json.avatarUrl;
|
||||
|
||||
// Extracted in newer versions
|
||||
this.token = jsonData.token;
|
||||
this.courses = jsonData.courses.map((courseJson: any) => new Course(courseJson));
|
||||
|
||||
// Calculated grade level
|
||||
this.gradeLevel = GPAUtils.getGradeLevel(this.graduationYear);
|
||||
this.gradeLevelName = GPAUtils.gradeLevelName(this.gradeLevel);
|
||||
|
||||
// Generate default avatar
|
||||
if (this.avatarUrl == null || this.avatarUrl == '')
|
||||
{
|
||||
this.avatarUrl = `https://www.gravatar.com/avatar/${md5(this.emails[0])}?d=404` + encodeURIComponent(
|
||||
`https://ui-avatars.com/api/${this.firstName.charAt(0)}${this.lastName.charAt(0)}/128`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
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,20 +0,0 @@
|
||||
|
||||
export default class CacheUtils
|
||||
{
|
||||
map: Map<string, any> = new Map();
|
||||
|
||||
/**
|
||||
* Get a cached value, or if not cached, cache it.
|
||||
*
|
||||
* @param name Name of the cached value
|
||||
* @param callback Callback function
|
||||
*/
|
||||
public get(name: string, callback: () => any)
|
||||
{
|
||||
if (!this.map.has(name))
|
||||
{
|
||||
this.map.set(name, callback());
|
||||
}
|
||||
return this.map.get(name);
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
import Navigation from '@/components/navigation/navigation';
|
||||
import Constants from '@/constants';
|
||||
import {isNumeric} from '@/logic/utils/general-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 LEVEL_SPORT = {level: 'Sport', scaleUp: -1};
|
||||
const LEVEL_NONE = {level: 'None', scaleUp: -1};
|
||||
const LEVEL_UNKNOWN = {level: 'Unknown', scaleUp: -1};
|
||||
|
||||
export class CourseUtils
|
||||
{
|
||||
/**
|
||||
* Get the begin date of the selected term
|
||||
*/
|
||||
static getTermBeginDate()
|
||||
{
|
||||
let selected = Navigation.instance.getSelectedTerm();
|
||||
|
||||
return selected == -1 ? Constants.TERMS[0] : Constants.TERMS[selected];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the end date of the selected term
|
||||
*/
|
||||
static getTermEndDate()
|
||||
{
|
||||
let selected = Navigation.instance.getSelectedTerm();
|
||||
|
||||
return selected == -1 ? Constants.TERMS[4] : Constants.TERMS[selected + 1];
|
||||
}
|
||||
|
||||
static getLevelID(level: string)
|
||||
{
|
||||
if (level == undefined) return -1;
|
||||
|
||||
level = level.toLowerCase();
|
||||
|
||||
if (level == 'ap' || level == 'advanced placement') return 1;
|
||||
if (level == 'h' || level == 'honors') return 2;
|
||||
if (level == 'a' || level == 'acc' || level == 'accelerated') return 3;
|
||||
if (level == 'cp' || level == 'college prep') return 4;
|
||||
|
||||
if (level == 'club') return 101;
|
||||
if (level == 'sport') return 102;
|
||||
|
||||
if (level == 'none') return 201;
|
||||
|
||||
if (isNumeric(level)) return +level;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full name of a level from short name
|
||||
*
|
||||
* @param level Any level name
|
||||
*/
|
||||
static getLevelFullName(level: string)
|
||||
{
|
||||
switch (this.getLevelID(level))
|
||||
{
|
||||
case 1: return 'AP';
|
||||
case 2: return 'Honors';
|
||||
case 3: return 'Accelerated';
|
||||
case 4: return 'CP';
|
||||
case 101: return 'Club';
|
||||
case 102: return 'Sport';
|
||||
case 201: return 'None';
|
||||
default: return '--';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full name of a level from short name
|
||||
*
|
||||
* @param level Any level name
|
||||
*/
|
||||
static getLevel(level: string)
|
||||
{
|
||||
switch (this.getLevelID(level))
|
||||
{
|
||||
case 1: return LEVEL_AP;
|
||||
case 2: return LEVEL_H;
|
||||
case 3: return LEVEL_A;
|
||||
case 4: return LEVEL_CP;
|
||||
case 101: return LEVEL_CLUB;
|
||||
case 102: return LEVEL_SPORT;
|
||||
case 201: return LEVEL_NONE;
|
||||
default: return LEVEL_UNKNOWN;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
export class FormatUtils
|
||||
{
|
||||
/**
|
||||
* Limit string length
|
||||
*
|
||||
* @param str String
|
||||
* @param length Max length
|
||||
*/
|
||||
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(/&/g, '&');
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
|
||||
export function findLastIndex<T>(array: T[], callback: (v: T) => boolean): number
|
||||
{
|
||||
let arr2 = array.slice().reverse();
|
||||
let result = arr2.findIndex(callback);
|
||||
return result == -1 ? -1 : arr2.length - result - 1;
|
||||
}
|
||||
|
||||
export function isNumeric(str: string)
|
||||
{
|
||||
return !isNaN(parseFloat(str)) && isFinite(+str);
|
||||
}
|
||||
@@ -1,232 +0,0 @@
|
||||
import Course, {Assignment, Grading} from '@/logic/course';
|
||||
|
||||
export interface Scale
|
||||
{
|
||||
min: number
|
||||
letter: string
|
||||
gp: number
|
||||
}
|
||||
|
||||
/**
|
||||
* This is an utility class to calculate GPA.
|
||||
*/
|
||||
export class GPAUtils
|
||||
{
|
||||
// [[Min score, Letter grade, Base GPA], ...]
|
||||
public static SCALE: Scale[] =
|
||||
[
|
||||
{min: 96.5, letter: 'A+', gp: 4.00},
|
||||
{min: 92.5, letter: 'A' , gp: 3.75},
|
||||
{min: 89.5, letter: 'A-', gp: 3.50},
|
||||
{min: 86.5, letter: 'B+', gp: 3.25},
|
||||
{min: 82.5, letter: 'B' , gp: 3.00},
|
||||
{min: 79.5, letter: 'B-', gp: 2.75},
|
||||
{min: 76.5, letter: 'C+', gp: 2.50},
|
||||
{min: 72.5, letter: 'C' , gp: 2.25},
|
||||
{min: 70.5, letter: 'C-', gp: 2.00},
|
||||
{min: 69.5, letter: 'D' , gp: 1.00},
|
||||
{min: 0 , letter: 'F' , gp: 0.00}
|
||||
];
|
||||
|
||||
/**
|
||||
* Calculate GPA for a list of couses
|
||||
*
|
||||
* @param coursesOriginal List of courses
|
||||
*/
|
||||
public static getGPA(coursesOriginal: Course[]): {gpa: number, accurate: boolean, max: number}
|
||||
{
|
||||
// Clone array
|
||||
let courses: Course[] = [];
|
||||
|
||||
// Accurate or not
|
||||
let accurate: boolean = true;
|
||||
|
||||
// Remove all courses that does not have a grade
|
||||
coursesOriginal.forEach(course =>
|
||||
{
|
||||
if (course.letterGrade == null || course.letterGrade == '')
|
||||
{
|
||||
accurate = false;
|
||||
}
|
||||
else if (course.level != 'none' && !isNaN(course.numericGrade))
|
||||
{
|
||||
courses.push(course);
|
||||
}
|
||||
});
|
||||
|
||||
// If no course have grade, return -1
|
||||
if (courses.length == 0)
|
||||
{
|
||||
return {gpa: -1, accurate: false, max: -1};
|
||||
}
|
||||
|
||||
// Count total GPA
|
||||
let totalGPA = 0;
|
||||
let maxTotal = 0;
|
||||
courses.forEach(course =>
|
||||
{
|
||||
totalGPA += this.getGP(course, course.numericGrade);
|
||||
maxTotal += this.getGP(course, 'A+');
|
||||
});
|
||||
|
||||
// Get average GPA, round to two decimal places
|
||||
let gpa = Math.round(totalGPA / courses.length * 100) / 100;
|
||||
let maxGPA = Math.round(maxTotal / courses.length * 100) / 100;
|
||||
|
||||
// Return results
|
||||
return {gpa: gpa, accurate: accurate, max: maxGPA};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate GPA for a course
|
||||
*
|
||||
* @param course Course
|
||||
* @param letterGrade Letter grade
|
||||
*/
|
||||
public static getGP(course: Course, letterGrade: string | number): number
|
||||
{
|
||||
// Get scale
|
||||
let scale = this.findScale(letterGrade);
|
||||
|
||||
// No scale
|
||||
if (scale == undefined) return -1;
|
||||
|
||||
// Add scaleUp if not failed.
|
||||
return scale.gp == 0 ? 0 : scale.gp + course.scaleUp;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the scale for a grade
|
||||
*
|
||||
* @param grade Letter grade or numeric grade
|
||||
*/
|
||||
public static findScale(grade: string | number): Scale | undefined
|
||||
{
|
||||
// Letter grade
|
||||
if (typeof grade == 'string')
|
||||
{
|
||||
return this.SCALE.find(scale => scale.letter == grade);
|
||||
}
|
||||
|
||||
// Numeric grade
|
||||
return this.SCALE.find(scale => grade >= scale.min);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the total-mean (total/max) average
|
||||
*
|
||||
* @param assignments
|
||||
*/
|
||||
public static getTotalMeanAverage(assignments: Assignment[])
|
||||
{
|
||||
let score = 0;
|
||||
let max = 0;
|
||||
|
||||
// Loop through assignments
|
||||
assignments.forEach(assignment =>
|
||||
{
|
||||
// If assignment should be displayed
|
||||
if (!assignment.graded) return;
|
||||
|
||||
// Record scores
|
||||
score += assignment.score;
|
||||
max += assignment.scoreMax;
|
||||
});
|
||||
|
||||
// Return
|
||||
return +(score / max * 100).toFixed(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the percent type
|
||||
*
|
||||
* @param grading
|
||||
* @param assignments
|
||||
*/
|
||||
public static getPercentTypeAverage(grading: Grading, assignments: Assignment[])
|
||||
{
|
||||
let typeScores: {[index: string]: any} = {};
|
||||
let typeCounts: {[index: string]: any} = {};
|
||||
|
||||
// Loop through assignments
|
||||
assignments.forEach(assignment =>
|
||||
{
|
||||
// If assignment should be displayed
|
||||
if (!assignment.graded) return;
|
||||
|
||||
// Record scores
|
||||
if (typeScores[assignment.type] == undefined) typeScores[assignment.type] = 0;
|
||||
typeScores[assignment.type] += assignment.score / assignment.scoreMax;
|
||||
|
||||
if (typeCounts[assignment.type] == undefined) typeCounts[assignment.type] = 0;
|
||||
typeCounts[assignment.type] ++;
|
||||
});
|
||||
|
||||
// Count total percentage (This is to avoid less than expected cases)
|
||||
// Eg. If HW = 25% and Quiz = 75%, I have 1 hw and 0 quiz
|
||||
// Without total percentage, the avg grade I get is 25%.
|
||||
let totalPercentage = 0;
|
||||
for (let type in grading.weightingMap)
|
||||
{
|
||||
if (typeScores[type] != undefined)
|
||||
{
|
||||
totalPercentage += grading.weightingMap[type];
|
||||
}
|
||||
}
|
||||
|
||||
// Count
|
||||
let score = 0;
|
||||
for (let type in typeScores)
|
||||
{
|
||||
let typeFactor = grading.weightingMap[type] / totalPercentage;
|
||||
score += typeScores[type] * typeFactor / typeCounts[type];
|
||||
}
|
||||
|
||||
// Add average to the row
|
||||
return +(score * 100).toFixed(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current school year
|
||||
*/
|
||||
public static getSchoolYear(): number
|
||||
{
|
||||
// Get current year
|
||||
let currentYear = new Date().getFullYear();
|
||||
|
||||
// Convert current year to current school year: +1 if it's after August
|
||||
if (new Date().getMonth() > 7) currentYear ++;
|
||||
|
||||
return currentYear;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get grade level from graduation year
|
||||
*
|
||||
* @param graduationYear
|
||||
*/
|
||||
public static getGradeLevel(graduationYear: number): number
|
||||
{
|
||||
// Calculate grade level
|
||||
return 12 - (graduationYear - this.getSchoolYear());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get grade level name from grade level. (Eg. Freshman, Sophomore, etc.)
|
||||
*
|
||||
* @param gradeLevel
|
||||
*/
|
||||
public static gradeLevelName(gradeLevel: number): string
|
||||
{
|
||||
switch (gradeLevel)
|
||||
{
|
||||
case 9: return 'Freshman';
|
||||
case 10: return 'Sophomore';
|
||||
case 11: return 'Junior';
|
||||
case 12: return 'Senior';
|
||||
|
||||
default: return 'Grade ' + gradeLevel;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
import Constants from '@/constants';
|
||||
import App from '@/components/app/app';
|
||||
|
||||
export default class GraphUtils
|
||||
{
|
||||
static DOT = '<span style="display:inline-block;margin-right:5px;border-radius:10px;width:9px;height:9px;background-color:{color}"></span>';
|
||||
|
||||
/**
|
||||
* Base settings
|
||||
*
|
||||
* @param title
|
||||
* @param subtitle
|
||||
*/
|
||||
static getBaseSettings(title?: String, subtitle?: String)
|
||||
{
|
||||
return {
|
||||
// Color
|
||||
color: Constants.THEME.colors,
|
||||
backgroundColor: 'transparent',
|
||||
|
||||
// Title
|
||||
title:
|
||||
{
|
||||
show: title != null,
|
||||
textStyle:
|
||||
{
|
||||
fontSize: 13
|
||||
},
|
||||
text: title,
|
||||
subtext: subtitle,
|
||||
x: 'center'
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get term mark lines
|
||||
*/
|
||||
static getTermLines()
|
||||
{
|
||||
return {
|
||||
silent: true,
|
||||
symbol: 'none',
|
||||
lineStyle: {color: Constants.THEME.colors[2]},
|
||||
animationDuration: 500,
|
||||
data: Constants.TERMS.map((term, index) =>
|
||||
{
|
||||
return {xAxis: term.getTime(), label: {formatter: `Term ${index + 1}`}}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get mark areas for percentage scores
|
||||
*/
|
||||
static getGradeMarkAreas(opacity: number)
|
||||
{
|
||||
// TODO: Auto update after switching dark mode (possibly by refreshing)
|
||||
opacity = App.instance.darkMode ? 0.1 : opacity;
|
||||
|
||||
return {
|
||||
silent: true,
|
||||
data:
|
||||
[
|
||||
// Above 100
|
||||
[{itemStyle: {color: 'rgba(230,253,255)', opacity: opacity}, yAxis: 120}, {yAxis: 100}],
|
||||
// 90 to 100
|
||||
[{itemStyle: {color: 'rgba(241,255,237)', opacity: opacity}, yAxis: 100}, {yAxis: 90}],
|
||||
// 80 to 90
|
||||
[{itemStyle: {color: 'rgba(255,250,216)', opacity: opacity}, yAxis: 90}, {yAxis: 80}],
|
||||
// 70 to 80
|
||||
[{itemStyle: {color: 'rgba(255,225,199)', opacity: opacity}, yAxis: 80}, {yAxis: 70}],
|
||||
// Below 70 (Fail)
|
||||
[{itemStyle: {color: 'rgb(255,190,184)', opacity: opacity}, yAxis: 70}, {yAxis: -100}]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Text style for pie graphs or radar graphs
|
||||
*/
|
||||
static pieTextStyle()
|
||||
{
|
||||
return {
|
||||
fontSize: 14,
|
||||
textShadowColor: '#cfcfcf',
|
||||
textShadowBlur: 2,
|
||||
textShadowOffsetX: 1,
|
||||
textShadowOffsetY: 1,
|
||||
backgroundColor: '#f6f6f6',
|
||||
borderRadius: 3,
|
||||
padding: [3, 5]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS shadow string (extraCssText) for tooltip
|
||||
*/
|
||||
static tooltipCssShadow()
|
||||
{
|
||||
return {extraCssText: 'box-shadow: 0 0 3px rgba(0, 0, 0, 0.3);'}
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import Constants from '@/constants';
|
||||
import LoginUser from '@/logic/login-user';
|
||||
|
||||
export class HttpUtils
|
||||
{
|
||||
public user: LoginUser;
|
||||
|
||||
public post(node: string, body: any): Promise<any>
|
||||
{
|
||||
// Add token
|
||||
if (this.user != null) body['token'] = this.user.token;
|
||||
|
||||
// Create promise
|
||||
return new Promise<any>((resolve, reject) =>
|
||||
{
|
||||
// Fetch request
|
||||
fetch(`${Constants.API_URL}${node}`, {method: 'POST', body: JSON.stringify(body)}).then(res =>
|
||||
{
|
||||
// Get response body text
|
||||
res.text().then(text =>
|
||||
{
|
||||
// Parse response
|
||||
let response = JSON.parse(text);
|
||||
resolve(response);
|
||||
})
|
||||
.catch(reject)
|
||||
})
|
||||
.catch(reject)
|
||||
});
|
||||
}
|
||||
|
||||
public get(url: string): Promise<any>
|
||||
{
|
||||
// Create promise
|
||||
return new Promise<any>((resolve, reject) =>
|
||||
{
|
||||
// Fetch request
|
||||
fetch(url, {method: 'GET'}).then(res =>
|
||||
{
|
||||
// Get response body text
|
||||
res.text().then(text =>
|
||||
{
|
||||
// Parse response
|
||||
let response = JSON.parse(text);
|
||||
resolve(response);
|
||||
})
|
||||
.catch(reject)
|
||||
})
|
||||
.catch(reject)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
export default class VersionUtils
|
||||
{
|
||||
/**
|
||||
* Compare two version numbers
|
||||
*
|
||||
* Eg.
|
||||
* compare('0.1.2', '0.1.3') = -1
|
||||
* compare('1.0.0', '0.1.3') = 1
|
||||
* compare('0.0.1', '0.0.1') = 0
|
||||
*
|
||||
* @param ver1 Version 1
|
||||
* @param ver2 Version 2
|
||||
* @return number (-1 if ver1 < ver2), (1 if ver1 > ver2), (0 if equal)
|
||||
*/
|
||||
public static compare(ver1: string, ver2: string): number
|
||||
{
|
||||
// Equal case
|
||||
if (ver1 == ver2) return 0;
|
||||
|
||||
// Split
|
||||
let split1 = ver1.split('.');
|
||||
let split2 = ver2.split('.');
|
||||
|
||||
// Detect each number
|
||||
for (let i in split1)
|
||||
{
|
||||
// Get numbers
|
||||
let num1 = split1[i];
|
||||
let num2 = split2[i];
|
||||
|
||||
// Current number is equal
|
||||
if (num1 == num2) continue;
|
||||
|
||||
// Current number is different
|
||||
return +num1 < +num2 ? -1 : 1;
|
||||
}
|
||||
|
||||
// Equal
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import Vue from 'vue';
|
||||
import ElementUI from 'element-ui';
|
||||
import App from './components/app/app.vue';
|
||||
import VueCookies from 'vue-cookies';
|
||||
|
||||
const VCharts = require('v-charts');
|
||||
|
||||
Vue.config.productionTip = false;
|
||||
|
||||
// Use Element UI
|
||||
Vue.use(ElementUI, {locale: 'en-us'});
|
||||
|
||||
// Use VCharts
|
||||
Vue.use(VCharts);
|
||||
|
||||
// Use Cookies
|
||||
Vue.use(VueCookies);
|
||||
|
||||
// Init app
|
||||
new Vue({
|
||||
render: (h) => h(App),
|
||||
}).$mount('#app');
|
||||
@@ -1,81 +0,0 @@
|
||||
|
||||
#course-list
|
||||
{
|
||||
margin-right: 20px;
|
||||
height: 70vh;
|
||||
|
||||
.padding-fix
|
||||
{
|
||||
// Fix width issue
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.header
|
||||
{
|
||||
.text
|
||||
{
|
||||
margin-top: 15px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.search
|
||||
{
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.list
|
||||
{
|
||||
overflow: scroll;
|
||||
overflow-x: hidden;
|
||||
height: 377px;
|
||||
}
|
||||
|
||||
// Remove scrollbar
|
||||
.list::-webkit-scrollbar
|
||||
{
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.item
|
||||
{
|
||||
text-align: left;
|
||||
margin-bottom: 15px;
|
||||
|
||||
background: #fbfbfb;
|
||||
border-radius: 4px;
|
||||
padding: 5px 10px;
|
||||
|
||||
.name
|
||||
{
|
||||
// Text too long
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.data
|
||||
{
|
||||
// text-align: right;
|
||||
color: #a5a5a5;
|
||||
|
||||
span
|
||||
{
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
// text-align: left;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cards
|
||||
.left
|
||||
{
|
||||
margin-left: 20px;
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator'
|
||||
import App from '@/components/app/app';
|
||||
import CourseInfo, {ClassInfo, UniqueCourse} from '@/logic/course-info';
|
||||
import {GPAUtils} from '@/logic/utils/gpa-utils';
|
||||
// @ts-ignore
|
||||
import SearchSettingsComponent, {SearchSettings} from '@/pages/course-selection/pages/search-settings.vue';
|
||||
import Welcome from '@/pages/course-selection/pages/welcome.vue';
|
||||
import CourseDetail from '@/pages/course-selection/pages/course-detail.vue';
|
||||
import LoadingSpinner from '@/components/loading-spinner.vue';
|
||||
|
||||
@Component({components: {SearchSettings: SearchSettingsComponent, Welcome, CourseDetail, LoadingSpinner}})
|
||||
export default class CourseSelection extends Vue
|
||||
{
|
||||
@Prop({required: true}) app: App
|
||||
|
||||
search: string = ''
|
||||
courseInfo: CourseInfo[] = []
|
||||
courseIdIndex: any = {} // Map<CourseID, index in courseInfo>
|
||||
directory: {gradeLevel: number, classes: string}[] = []
|
||||
classes: ClassInfo[] = []
|
||||
loading = true
|
||||
|
||||
courseListHeight: number = 0;
|
||||
cardsHeight: number = 0;
|
||||
|
||||
openedPage: string = '';
|
||||
settings: SearchSettings = new SearchSettings();
|
||||
activeCourse: UniqueCourse = new UniqueCourse('', [], -1);
|
||||
|
||||
/**
|
||||
* Called before rendering
|
||||
*/
|
||||
created()
|
||||
{
|
||||
// Update width dynamically
|
||||
window.addEventListener('resize', this.updateHeight);
|
||||
|
||||
// Get courses
|
||||
App.http.post('/course-info', {}).then(result =>
|
||||
{
|
||||
if (result.success)
|
||||
{
|
||||
// Parse data
|
||||
this.classes = result.data.classes.map((json: any) => new ClassInfo(json));
|
||||
this.courseInfo = result.data.courseInfos.map((json: any, index: number) =>
|
||||
{
|
||||
let info = new CourseInfo(json);
|
||||
|
||||
// Index
|
||||
info.courseIds.forEach(id =>
|
||||
{
|
||||
this.courseIdIndex[id] = index;
|
||||
|
||||
// Add class info into course
|
||||
let classInfo = this.classes.find(c => c.id == id)
|
||||
if (classInfo == null) return;
|
||||
info.classes.push(classInfo);
|
||||
});
|
||||
return info;
|
||||
});
|
||||
this.directory = result.data.studentInfos;
|
||||
|
||||
// Use directory data
|
||||
this.directory.forEach(d =>
|
||||
{
|
||||
d.classes.split('|').forEach(classId =>
|
||||
{
|
||||
// Get info by class id
|
||||
let info = this.courseInfo[this.courseIdIndex[+classId]];
|
||||
if (info as any != null)
|
||||
{
|
||||
// Add grade level
|
||||
if (!info.gradeLevels.includes(d.gradeLevel))
|
||||
{
|
||||
info.gradeLevels.push(d.gradeLevel);
|
||||
}
|
||||
|
||||
// Count enrollments
|
||||
info.enrollments ++;
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
this.loading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Called on destroy
|
||||
*/
|
||||
destroyed()
|
||||
{
|
||||
// Remove width updater
|
||||
window.removeEventListener('resize', this.updateHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called on vue update
|
||||
*/
|
||||
updated()
|
||||
{
|
||||
this.updateHeight()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update header height. (CSS doesn't work)
|
||||
*/
|
||||
updateHeight()
|
||||
{
|
||||
// Get element
|
||||
let cl = this.$refs.cl as Vue;
|
||||
if (cl as any == null) return;
|
||||
let el = cl.$el;
|
||||
|
||||
// Calculate height
|
||||
this.cardsHeight = window.innerHeight - el.getBoundingClientRect().top - 20;
|
||||
this.courseListHeight = this.cardsHeight - 15 - 102;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the settings page.
|
||||
*/
|
||||
openSettings()
|
||||
{
|
||||
this.openedPage = this.openedPage == 'settings' ? '' : 'settings';
|
||||
}
|
||||
|
||||
/**
|
||||
* Open course page.
|
||||
*/
|
||||
openCourse(course: UniqueCourse)
|
||||
{
|
||||
if (this.activeCourse == course)
|
||||
{
|
||||
this.openedPage = '';
|
||||
this.activeCourse = null as any as UniqueCourse;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.activeCourse = course;
|
||||
this.openedPage = 'course'
|
||||
}
|
||||
}
|
||||
|
||||
get filteredCourses()
|
||||
{
|
||||
let year = GPAUtils.getSchoolYear();
|
||||
|
||||
return this.courseInfo.filter(c =>
|
||||
c.uniqueName.toLowerCase().includes(this.search.toLowerCase()) &&
|
||||
c.level != null && this.settings.levels.includes(c.level) &&
|
||||
c.year == year &&
|
||||
(this.settings.showAllCourses || c.gradeLevels.includes(this.app.user.gradeLevel + 1))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets unique courses by name, even though many different teachers might teach it.
|
||||
*/
|
||||
get uniqueCourses(): UniqueCourse[]
|
||||
{
|
||||
let names: string[] = [];
|
||||
let list: UniqueCourse[] = [];
|
||||
|
||||
this.filteredCourses.forEach(c =>
|
||||
{
|
||||
// Create the course list if doesn't exist
|
||||
if (!names.includes(c.uniqueName))
|
||||
{
|
||||
names.push(c.uniqueName);
|
||||
list.push(new UniqueCourse(c.uniqueName, [], 0))
|
||||
}
|
||||
|
||||
// Add the course
|
||||
list[names.indexOf(c.uniqueName)].courses.push(c);
|
||||
list[names.indexOf(c.uniqueName)].enrollments += c.enrollments;
|
||||
})
|
||||
|
||||
// Sorting
|
||||
switch (this.settings.sortBy)
|
||||
{
|
||||
case 'Popularity':
|
||||
{
|
||||
list.sort((a, b) => b.enrollments - a.enrollments);
|
||||
break
|
||||
}
|
||||
default:
|
||||
{
|
||||
list.sort((a, b) => a.name.localeCompare(b.name));
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
<template>
|
||||
<div id="course-selection">
|
||||
<div v-if="loading" class="loading vertical-center" style="height: 100%">
|
||||
<LoadingSpinner style="left: 0"/>
|
||||
</div>
|
||||
|
||||
<el-row v-else>
|
||||
<el-col :span="16" class="overall-span">
|
||||
<el-card class="left" :style="{height: cardsHeight + 'px'}">
|
||||
<SearchSettings v-if="openedPage === 'settings'" ref="settings" :settings="settings"/>
|
||||
<Welcome v-if="openedPage === ''" :app="app"/>
|
||||
<CourseDetail v-if="openedPage === 'course'" :unique-course="activeCourse"/>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- Course list card -->
|
||||
<el-col :span="8" class="overall-span">
|
||||
<el-card id="course-list" class="right" ref="cl" body-style="padding: 0" :style="{height: cardsHeight + 'px'}">
|
||||
<div class="header padding-fix">
|
||||
<div class="text">Course List</div>
|
||||
|
||||
<!-- Search -->
|
||||
<el-input class="search" placeholder="Search..." prefix-icon="el-icon-search" v-model="search">
|
||||
<el-button slot="append" icon="el-icon-s-tools" @click="openSettings"/>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<!-- Actual course list -->
|
||||
<div class="list padding-fix" :style="{height: courseListHeight + 'px'}">
|
||||
<!-- Every course -->
|
||||
<div v-for="(course, index) in uniqueCourses" class="item vertical-center clickable unselectable"
|
||||
@click="openCourse(course)">
|
||||
|
||||
<div class="name">{{course.name}}</div>
|
||||
<div class="data">
|
||||
<span class="classes"><i class="el-icon-s-home"/> {{course.classes.length}}</span>
|
||||
<span class="teachers"><i class="el-icon-user-solid"/> {{course.courses.length}}</span>
|
||||
<span class="enrollments"><i class="el-icon-user"/> {{course.enrollments}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./course-selection.ts" lang="ts"/>
|
||||
<style src="./course-selection.scss" lang="scss" scoped/>
|
||||
@@ -1,269 +0,0 @@
|
||||
<template>
|
||||
<div id="course-detail">
|
||||
<div class="header">Course: <span style="color: #229fff">{{uniqueCourse.name}}</span></div>
|
||||
<el-divider class="divider"><i class="el-icon-reading"></i></el-divider>
|
||||
|
||||
<!-- All course-infos -->
|
||||
<div class="item clickable unselectable" v-for="c in sortedCourses" @click="openDetails(c)">
|
||||
<div class="float-left">
|
||||
<div>{{c.levelFull}} - <i>{{c.teacher}}</i></div>
|
||||
<div class="info">
|
||||
<span class="name">{{c.name}} : </span>
|
||||
<span class="classes"><i class="el-icon-s-home"/> {{c.classes.length}}</span>
|
||||
<span class="enrollments"><i class="el-icon-user"/> {{c.enrollments}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="float-right">
|
||||
<LoadingSpinner v-if="c.rating == null" class="loading" size="30" :centered="false"/>
|
||||
<div v-else class="rating">
|
||||
<span v-if="c.rating.totalCount === 0" class="text">No ratings yet...</span>
|
||||
<span v-else class="stars">
|
||||
<StarRating :score="c.rating.overallRating"></StarRating>
|
||||
<span class="info">
|
||||
<span class="numeric-rating">{{c.rating.overallRating.toFixed(2)}} / 5</span>
|
||||
<span>({{c.rating.totalCount}} rating{{c.rating.totalCount > 1?'s':''}})</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Detail / Comments Popup -->
|
||||
<el-dialog id="detail-popup" v-if="detailsCourse" :visible="detailsCourse != null" width="50%" top="10vh"
|
||||
:before-close="closeDetails">
|
||||
<span slot="title" class="header">
|
||||
<div class="title">Ratings for {{detailsCourse.name}}</div>
|
||||
<span class="subtitle">And for {{detailsCourse.teacher}}</span>
|
||||
</span>
|
||||
|
||||
<div class="rating-item" v-for="(criteria, index) of ratingCriteria">
|
||||
<div class="title float-left">{{criteria.title}}</div>
|
||||
|
||||
<div class="stars float-right">
|
||||
<span class="info numeric-rating">{{rating.ratingAverages[index].toFixed(2)}} / 5</span>
|
||||
<StarRating :score="rating.ratingAverages[index]" style="display: inline-block"></StarRating>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="comments">
|
||||
<div class="header">
|
||||
Comments
|
||||
</div>
|
||||
|
||||
<LoadingSpinner v-if="detailsComments == null"/>
|
||||
<div class="comment" v-else v-for="comment of detailsComments">
|
||||
<div class="user">
|
||||
<i class="el-icon-user-solid"/>
|
||||
{{comment.firstName}} {{comment.lastName}}:
|
||||
<span class="info numeric-rating" style="margin-left: 5px">{{comment.averageRating.toFixed(2)}} / 5</span>
|
||||
</div>
|
||||
<div class="text">
|
||||
<blockquote>{{comment.comment}}</blockquote>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator'
|
||||
import CourseInfo, {AnalyzedRating, CourseInfoRating, RATING_CRITERIA, UniqueCourse} from '@/logic/course-info';
|
||||
import App from '@/components/app/app';
|
||||
import course from '@/logic/course';
|
||||
import LoadingSpinner from '@/components/loading-spinner.vue';
|
||||
import loading from '@/components/overlays/loading.vue';
|
||||
import StarRating from '@/components/star-rating.vue';
|
||||
@Component({components: {StarRating, LoadingSpinner}})
|
||||
export default class CourseDetail extends Vue
|
||||
{
|
||||
@Prop({required: true}) uniqueCourse: UniqueCourse;
|
||||
|
||||
detailsCourse: CourseInfo = null as any as CourseInfo
|
||||
detailsComments: CourseInfoRating[] = null as any as []
|
||||
|
||||
get ratingCriteria() {return RATING_CRITERIA}
|
||||
get rating() {return this.detailsCourse.rating}
|
||||
|
||||
mounted()
|
||||
{
|
||||
this.checkRatings()
|
||||
}
|
||||
|
||||
updated()
|
||||
{
|
||||
this.checkRatings()
|
||||
}
|
||||
|
||||
checkRatings()
|
||||
{
|
||||
// Load ratings
|
||||
this.sortedCourses.forEach(c =>
|
||||
{
|
||||
// Already has rating
|
||||
if (c.rating as any != null) return;
|
||||
|
||||
// Get rating
|
||||
App.http.post('/course-info/rating/get', {condition: 'course', value: c.id_ci}).then(result =>
|
||||
{
|
||||
if (result.success)
|
||||
{
|
||||
// Assign rating
|
||||
c.rating = new AnalyzedRating(result.data);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.$message.error(`Rating data for ${c.name} / ${c.teacher} failed to load.`)
|
||||
console.log(result.data);
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
get sortedCourses(): CourseInfo[]
|
||||
{
|
||||
return this.uniqueCourse.courses.sort((a, b) => a.levelID - b.levelID);
|
||||
}
|
||||
|
||||
openDetails(course: CourseInfo)
|
||||
{
|
||||
let c = this.detailsCourse = this.detailsCourse == course ? null as any as CourseInfo : course;
|
||||
|
||||
// Load comments
|
||||
App.http.post('/course-info/rating/get', {condition: 'course-comments', value: c.id_ci}).then(result =>
|
||||
{
|
||||
if (result.success)
|
||||
{
|
||||
this.detailsComments = result.data.map((r:any) => new CourseInfoRating(r));
|
||||
}
|
||||
else
|
||||
{
|
||||
this.$message.error(`Rating data for ${c.name} / ${c.teacher} failed to load.`)
|
||||
console.log(result.data);
|
||||
}
|
||||
})
|
||||
|
||||
// TODO: Finish comment section
|
||||
}
|
||||
|
||||
closeDetails()
|
||||
{
|
||||
this.detailsCourse = null as any as CourseInfo;
|
||||
this.detailsComments = null as any as []
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style src="./pages.scss" lang="scss" scoped/>
|
||||
<style lang="scss" scoped>
|
||||
.item
|
||||
{
|
||||
text-align: left;
|
||||
margin-bottom: 15px;
|
||||
background: #f8fdff;
|
||||
border-radius: 4px;
|
||||
padding: 5px 10px;
|
||||
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.info
|
||||
{
|
||||
font-size: 12px;
|
||||
color: gray;
|
||||
|
||||
.classes
|
||||
{
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.numeric-rating
|
||||
{
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.float-left
|
||||
{
|
||||
text-align: left;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.float-right
|
||||
{
|
||||
text-align: right;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.loading
|
||||
{
|
||||
margin-top: 5px !important;
|
||||
}
|
||||
|
||||
.rating
|
||||
{
|
||||
.text
|
||||
{
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
#detail-popup
|
||||
{
|
||||
text-align: left;
|
||||
|
||||
.header
|
||||
{
|
||||
.title
|
||||
{
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-bottom: -10px;
|
||||
}
|
||||
|
||||
.subtitle
|
||||
{
|
||||
margin-left: 10px;
|
||||
font-size: 12px;
|
||||
font-style: italic;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
|
||||
.rating-item
|
||||
{
|
||||
height: 30px;
|
||||
|
||||
.title
|
||||
{
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.rating-item:first-child
|
||||
{
|
||||
margin-top: -15px;
|
||||
}
|
||||
|
||||
.comments
|
||||
{
|
||||
margin-top: 40px;
|
||||
|
||||
.comment
|
||||
{
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
blockquote
|
||||
{
|
||||
padding: 0 1em;
|
||||
/* color: #6a737d; */
|
||||
border-left: .25em solid #dfe2e5;
|
||||
margin: 5px 0 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,16 +0,0 @@
|
||||
.header
|
||||
{
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
font-size: 24px;
|
||||
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.divider
|
||||
{
|
||||
margin-top: 20px !important;
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
<template>
|
||||
<div id="settings">
|
||||
<div class="header">Settings</div>
|
||||
<el-divider class="divider"><i class="el-icon-s-tools"></i></el-divider>
|
||||
|
||||
<div class="content">
|
||||
<el-switch v-model="settings.showAllCourses" class="item"
|
||||
active-text="Show all courses (including the ones not listed on your grade level)"/>
|
||||
|
||||
<div class="item">
|
||||
<span class="item-label">Sort by:</span>
|
||||
<el-radio-group v-model="settings.sortBy">
|
||||
<el-radio label="Name"></el-radio>
|
||||
<el-radio label="Popularity"></el-radio>
|
||||
<el-radio label="Classes"></el-radio>
|
||||
<el-radio label="Level"></el-radio>
|
||||
<el-radio disabled label="Peer Rating (Coming soon)"></el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
|
||||
<div class="item">
|
||||
<span class="item-label">Levels:</span>
|
||||
<el-checkbox-group v-model="settings.levels" style="display: inline-block;">
|
||||
<el-checkbox label="AP">AP</el-checkbox>
|
||||
<el-checkbox label="H">Honors</el-checkbox>
|
||||
<el-checkbox label="A">Accelerated</el-checkbox>
|
||||
<el-checkbox label="CP">CP</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator'
|
||||
import App from '@/components/app/app';
|
||||
|
||||
@Component
|
||||
export default class SearchSettingsComponent extends Vue
|
||||
{
|
||||
@Prop({required: true}) settings: SearchSettings;
|
||||
// TODO: Show all courses option
|
||||
}
|
||||
|
||||
export class SearchSettings
|
||||
{
|
||||
showAllCourses: boolean = App.instance.user.gradeLevel == 12;
|
||||
sortBy: string = 'Name'
|
||||
levels: string[] = ['AP','H','A','CP']
|
||||
}
|
||||
</script>
|
||||
|
||||
<style src="./pages.scss" lang="scss" scoped/>
|
||||
<style lang="scss" scoped>
|
||||
.content
|
||||
{
|
||||
text-align: left;
|
||||
|
||||
.item
|
||||
{
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
|
||||
.item-label,.el-radio,.el-checkbox
|
||||
{
|
||||
margin-right: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,63 +0,0 @@
|
||||
<template>
|
||||
<div id="welcome">
|
||||
<div class="header">Welcome</div>
|
||||
<el-divider class="divider"><i class="el-icon-cold-drink"></i></el-divider>
|
||||
|
||||
<div class="content" style="color: #ff3d3d" v-if="app.user.gradeLevel >= 12">
|
||||
You are a senior, what are you doing over here lol. <br>
|
||||
Unfortunately I can't help you with college course selection.<br>
|
||||
(But you can still view course ratings)<br><br>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<span style="color:#409EFF">
|
||||
This new page is designed to help you with your course selection for your {{nextGrade}} year,
|
||||
providing more information such as how many people are currently enrolled in a course.
|
||||
</span>
|
||||
<br><br>
|
||||
The courses displayed are from the current year,
|
||||
but since they are unlikely to change,
|
||||
they can provide a good view for the courses next year.
|
||||
However, this also means that the new courses
|
||||
and courses that didn't open this year are not going to be displayed here.
|
||||
For 2020, the new courses are Financial Algebra and Acc Psychology.
|
||||
Also, by default, only the courses that current {{nextGrade.toLowerCase()}}s take are displayed,
|
||||
and you can enable "show all courses" in settings if you want to see all courses.
|
||||
<br><br>
|
||||
<b>Notations:</b><br>
|
||||
<i class="el-icon-s-home"/>: How many classes (blocks) did the course open this year.<br>
|
||||
<i class="el-icon-user-solid"/>: How many teachers are teaching this course.<br>
|
||||
<i class="el-icon-user"/> How many students are enrolled.<br>
|
||||
<br>
|
||||
<b>Sorting:</b><br>
|
||||
By default the courses are sorted by name,
|
||||
but you can change the settings to sort by popularity, by classes, or by level.
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator'
|
||||
import App from '@/components/app/app';
|
||||
import {GPAUtils} from '@/logic/utils/gpa-utils';
|
||||
|
||||
@Component
|
||||
export default class Welcome extends Vue
|
||||
{
|
||||
@Prop({required: true}) app: App
|
||||
|
||||
get nextGrade()
|
||||
{
|
||||
return GPAUtils.gradeLevelName(this.app.user.gradeLevel + 1)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style src="./pages.scss" lang="scss" scoped/>
|
||||
<style lang="scss" scoped>
|
||||
.content
|
||||
{
|
||||
text-align: justify;
|
||||
color: #585858;
|
||||
}
|
||||
</style>
|
||||
@@ -1,80 +0,0 @@
|
||||
<template>
|
||||
<div id="assignment-type-head">
|
||||
<el-card :body-style="{padding: '0px'}">
|
||||
<div id="type-info-card">
|
||||
<span id="type-name">{{type.name}}</span>
|
||||
<span class="type-average" v-if="type.graded">Average: {{type.percent}}%</span>
|
||||
<span class="type-average" v-if="!type.graded">No grades yet!</span>
|
||||
</div>
|
||||
|
||||
<AssignmentEntry v-for="(assignment, index) of filteredAssignments" :key="assignment.id"
|
||||
:assignment="assignment" :unread="false"
|
||||
:backgroundColor="index % 2 === 1 ? '#ffffff' : '#f7f7f7'" narrow="true">
|
||||
</AssignmentEntry>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import AssignmentEntry from '@/pages/overall/overall-course/assignment-entry/assignment-entry.vue';
|
||||
import {Assignment, AssignmentType} from '@/logic/course';
|
||||
|
||||
@Component({
|
||||
components: {AssignmentEntry}
|
||||
})
|
||||
export default class AssignmentTypeHead extends Vue
|
||||
{
|
||||
@Prop({required: true}) type: AssignmentType;
|
||||
@Prop({required: true}) assignments: Assignment[];
|
||||
|
||||
get filteredAssignments()
|
||||
{
|
||||
// Filter assignments to only this type
|
||||
return this.assignments.filter(a => a.typeId == this.type.id);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
#assignment-type-head
|
||||
{
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#type-info-card
|
||||
{
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
#type-name
|
||||
{
|
||||
// Font
|
||||
font-size: 22px;
|
||||
color: var(--main);
|
||||
|
||||
// Center
|
||||
height: 60px;
|
||||
line-height: 60px;
|
||||
|
||||
// Alignment
|
||||
padding-left: 20px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.type-average
|
||||
{
|
||||
// Font
|
||||
font-size: 14px;
|
||||
color: #8db3e4;
|
||||
|
||||
// Center
|
||||
height: 60px;
|
||||
line-height: 64px;
|
||||
|
||||
// Alignment
|
||||
float: left;
|
||||
margin-left: 15px;
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
@@ -1,26 +0,0 @@
|
||||
// Card
|
||||
.el-card.course-card
|
||||
{
|
||||
// Margins
|
||||
margin-right: 20px;
|
||||
margin-left: 20px;
|
||||
|
||||
// Limit name length
|
||||
white-space: nowrap;
|
||||
|
||||
// Expansion color
|
||||
background: #f4f6f9;
|
||||
}
|
||||
|
||||
.course-card-content.expand
|
||||
{
|
||||
// Top shadow
|
||||
// https://stackoverflow.com/questions/17572619/inset-box-shadow-only-on-one-side
|
||||
box-shadow: inset 0 7px 9px -7px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.type-graph
|
||||
{
|
||||
padding-top: 23px;
|
||||
height: 420px !important;
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
<template>
|
||||
<el-card id="course-card" class="course-card">
|
||||
<course-head :clickable="false" :course="course" :unread="countUnread()"/>
|
||||
|
||||
<div class="course-card-content expand">
|
||||
<el-row>
|
||||
<el-col :span="24" class="course-page-graph">
|
||||
<el-card class="large overall-line-card vertical-center">
|
||||
<course-scatter :course="course"/>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row>
|
||||
<el-col :span="12" class="course-page-graph">
|
||||
<el-card class="large overall-line-card vertical-center type-graph"
|
||||
body-style="padding: 0">
|
||||
<TypeRadar :course="course"/>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12" class="course-page-graph">
|
||||
<el-card class="large overall-line-card vertical-center type-graph"
|
||||
body-style="padding: 0">
|
||||
<TypePie :course="course"/>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<AssignmentTypeHead v-for="type in course.assignmentTypes" :key="type.id"
|
||||
:type="type" :assignments="course.assignments">
|
||||
</AssignmentTypeHead>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import CourseHead from '@/pages/overall/overall-course/course-head/course-head.vue';
|
||||
import CourseScatter from '@/pages/course/course-scatter/course-scatter';
|
||||
import AssignmentEntry from '@/pages/overall/overall-course/assignment-entry/assignment-entry.vue';
|
||||
import AssignmentTypeHead from '@/pages/course/assignment-type-head/assignment-type-head.vue';
|
||||
import Course, {Assignment} from '@/logic/course';
|
||||
import TypeRadar from '@/pages/course/type-radar/type-radar';
|
||||
import TypePie from '@/pages/course/type-pie/type-pie';
|
||||
|
||||
@Component({
|
||||
components: {TypeRadar, TypePie, AssignmentEntry, CourseHead, CourseScatter, AssignmentTypeHead}
|
||||
})
|
||||
export default class CoursePage extends Vue
|
||||
{
|
||||
@Prop({required: true}) course: Course;
|
||||
|
||||
private unread: number = -1;
|
||||
private unreadAssignments: Assignment[] = [];
|
||||
|
||||
/**
|
||||
* Count the number of unread assignments with cache
|
||||
*/
|
||||
countUnread(): number
|
||||
{
|
||||
if (this.unread == -1)
|
||||
{
|
||||
this.unreadAssignments = this.course.assignments.filter(a => a.unread);
|
||||
return this.unread = this.unreadAssignments.length;
|
||||
}
|
||||
else return this.unread;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style src="./course-page.scss" lang="scss" scoped/>
|
||||
@@ -1,170 +0,0 @@
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import Constants from '@/constants';
|
||||
import {FormatUtils} from '@/logic/utils/format-utils';
|
||||
import moment, {min, Moment} from 'moment';
|
||||
import Course, {Assignment} from '@/logic/course';
|
||||
import GraphUtils from '@/logic/utils/graph-utils';
|
||||
import chroma from 'chroma-js';
|
||||
import Navigation from '@/components/navigation/navigation';
|
||||
|
||||
@Component
|
||||
export default class CourseScatter extends Vue
|
||||
{
|
||||
@Prop({required: true}) course: Course;
|
||||
|
||||
/**
|
||||
* Override options
|
||||
*
|
||||
* @param options Original options (Unused)
|
||||
*/
|
||||
afterConfig(options: any)
|
||||
{
|
||||
return this.chartSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate settings
|
||||
*/
|
||||
get chartSettings()
|
||||
{
|
||||
let term = Navigation.instance.getSelectedTerm()
|
||||
|
||||
// Create settings
|
||||
let settings =
|
||||
{
|
||||
// Base settings
|
||||
...GraphUtils.getBaseSettings('Assignments', 'Assignment scores for ' + this.course.name),
|
||||
|
||||
// X axis represents course names
|
||||
xAxis:
|
||||
{
|
||||
type: 'time',
|
||||
axisLabel:
|
||||
{
|
||||
formatter: (name: any) => moment(name).format('MMM DD')
|
||||
},
|
||||
min: Constants.TERMS[term == -1 ? 0 : term].getTime(),
|
||||
max: term == -1 ? moment.min(moment(), moment(Constants.TERMS[4])).toDate().getTime() :
|
||||
Constants.TERMS[term + 1].getTime()
|
||||
},
|
||||
|
||||
// Y axis represents GPAs and MaxGPAs
|
||||
yAxis:
|
||||
{
|
||||
type: 'value',
|
||||
name: 'Percentage Score',
|
||||
nameLocation: 'center',
|
||||
nameGap: 38,
|
||||
axisLabel:
|
||||
{
|
||||
formatter: (name: any) => name + '%'
|
||||
},
|
||||
min: (value: any) => Math.floor(value.min) - 5,
|
||||
max: (value: any) => Math.min(Math.ceil(value.max), 110)
|
||||
},
|
||||
|
||||
// Tooltip
|
||||
tooltip:
|
||||
{
|
||||
...GraphUtils.tooltipCssShadow(),
|
||||
|
||||
trigger: 'axis',
|
||||
axisPointer:
|
||||
{
|
||||
type: 'cross'
|
||||
},
|
||||
formatter: (ps: any[]) => moment(ps[0].data[0]).format('MMM DD, YYYY') + '<br>' + ps.map(p =>
|
||||
`${GraphUtils.DOT.replace('{color}', p.color.colorStops[1].color)}
|
||||
${FormatUtils.limit(p.data[2].description, 22)}: ${p.data[1]}%<br>`).join('')
|
||||
},
|
||||
|
||||
// Legend
|
||||
legend:
|
||||
{
|
||||
bottom: 24,
|
||||
itemWidth: 14,
|
||||
textStyle:
|
||||
{
|
||||
color: '#777',
|
||||
fontSize: 11
|
||||
}
|
||||
},
|
||||
|
||||
// Data
|
||||
series: this.series()
|
||||
};
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get series data
|
||||
*/
|
||||
private series()
|
||||
{
|
||||
// Create scatter plots
|
||||
let series: any[] = this.course.assignmentTypes.filter(t => t.graded).map((type, i) =>
|
||||
{
|
||||
return {
|
||||
type: 'scatter',
|
||||
name: type.name,
|
||||
data: CourseScatter.assignmentsData(this.course.assignments.filter(a => a.typeId == type.id)),
|
||||
symbolSize: (data: any) => Math.max(Math.sqrt(type.weight * data[2].scoreMax / type.scoreMax) * 12, 12),
|
||||
|
||||
label:
|
||||
{
|
||||
emphasis:
|
||||
{
|
||||
show: true,
|
||||
formatter: (p: any) => p.data[2].description,
|
||||
position: 'top'
|
||||
}
|
||||
},
|
||||
|
||||
itemStyle:
|
||||
{
|
||||
normal:
|
||||
{
|
||||
opacity: 0.7,
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowOffsetY: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.2)',
|
||||
color:
|
||||
{
|
||||
type: 'radial',
|
||||
x: 0.4,
|
||||
y: 0.3,
|
||||
colorStops:
|
||||
[
|
||||
{offset: 0, color: chroma(Constants.THEME.colors[i]).set('hsl.l', 0.9).css()},
|
||||
{offset: 1, color: Constants.THEME.colors[i]}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Push other stuff
|
||||
series.push(
|
||||
{
|
||||
type: 'line',
|
||||
markLine: GraphUtils.getTermLines(),
|
||||
markArea: GraphUtils.getGradeMarkAreas(0.4)
|
||||
});
|
||||
|
||||
return series;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert assignments to series data
|
||||
*
|
||||
* @param assignments Assignments
|
||||
*/
|
||||
private static assignmentsData(assignments: Assignment[])
|
||||
{
|
||||
return assignments.filter(a => a.complete == 'Complete')
|
||||
.map(a => [a.time, (a.score / a.scoreMax * 100).toFixed(2), a]);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
<template>
|
||||
<div id="course-scatter">
|
||||
<ve-scatter height="450px" class="graph" :extend="{a: this.course.name}" :after-config="afterConfig"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./course-scatter.ts" lang="ts"></script>
|
||||
@@ -1,56 +0,0 @@
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import Constants from '@/constants';
|
||||
import Course from '@/logic/course';
|
||||
import GraphUtils from '@/logic/utils/graph-utils';
|
||||
|
||||
@Component
|
||||
export default class TypePie extends Vue
|
||||
{
|
||||
@Prop({required: true}) course: Course;
|
||||
|
||||
/**
|
||||
* Override options
|
||||
*
|
||||
* @param options Original options (Unused)
|
||||
*/
|
||||
afterConfig(options: any)
|
||||
{
|
||||
return this.chartSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate settings
|
||||
*/
|
||||
get chartSettings()
|
||||
{
|
||||
// Create settings
|
||||
let settings =
|
||||
{
|
||||
...GraphUtils.getBaseSettings('Assignment Type Weight',
|
||||
'How much each type of assignment affect your average'),
|
||||
|
||||
// Data
|
||||
series:
|
||||
{
|
||||
type: 'pie',
|
||||
avoidLabelOverlap: false,
|
||||
radius: ['40%', '60%'],
|
||||
center: ['50%', '55%'],
|
||||
label: GraphUtils.pieTextStyle(),
|
||||
data: this.course.assignmentTypes.filter(t => t.graded).map((t, i) => {return {
|
||||
value: t.weight,
|
||||
name: `${t.name}\n${t.weight}%`,
|
||||
itemStyle:
|
||||
{
|
||||
color: Constants.THEME.colors[i],
|
||||
opacity: 0.8,
|
||||
shadowColor: 'rgba(0,0,0,0.22)',
|
||||
shadowBlur: 10
|
||||
}
|
||||
}}).sort((a, b) => a.value - b.value)
|
||||
}
|
||||
};
|
||||
|
||||
return settings;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
<template>
|
||||
<div id="type-pie">
|
||||
<ve-pie height="420px" class="graph" :extend="{a: this.course.name}" :after-config="afterConfig"></ve-pie>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./type-pie.ts" lang="ts"></script>
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -1,102 +0,0 @@
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import Constants from '@/constants';
|
||||
import Course from '@/logic/course';
|
||||
import GraphUtils from '@/logic/utils/graph-utils';
|
||||
|
||||
@Component
|
||||
export default class TypeRadar extends Vue
|
||||
{
|
||||
@Prop({required: true}) course: Course;
|
||||
|
||||
/**
|
||||
* Override options
|
||||
*
|
||||
* @param options Original options (Unused)
|
||||
*/
|
||||
afterConfig(options: any)
|
||||
{
|
||||
return this.chartSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate settings
|
||||
*/
|
||||
get chartSettings()
|
||||
{
|
||||
let min = this.course.assignmentTypes.filter(t => t.graded).reduce((min, t) => Math.min(min, t.percent), 100);
|
||||
|
||||
// Create settings
|
||||
let settings =
|
||||
{
|
||||
...GraphUtils.getBaseSettings('Assignment Type Radar',
|
||||
'How are you doing for different types of assignment'),
|
||||
|
||||
// Radar settings
|
||||
radar:
|
||||
{
|
||||
// shape: 'circle',
|
||||
name:
|
||||
{
|
||||
textStyle: GraphUtils.pieTextStyle()
|
||||
},
|
||||
splitArea:
|
||||
{
|
||||
areaStyle:
|
||||
{
|
||||
color:
|
||||
[
|
||||
'rgb(255,161,151)',
|
||||
'rgb(255,190,184)',
|
||||
'rgba(255,225,199)',
|
||||
'rgba(255,250,216)',
|
||||
'rgba(241,255,237)',
|
||||
],
|
||||
opacity: 0.4
|
||||
}
|
||||
},
|
||||
indicator: this.course.assignmentTypes.filter(t => t.graded).map((t, i) => {return {
|
||||
name: `${t.name}\n${t.percent}%`,
|
||||
max: 100,
|
||||
min: min - 30,
|
||||
color: Constants.THEME.colors[i]
|
||||
}}),
|
||||
radius: '60%',
|
||||
center: ['50%', '55%']
|
||||
},
|
||||
|
||||
// Data
|
||||
series:
|
||||
{
|
||||
type: 'radar',
|
||||
data:
|
||||
[
|
||||
{
|
||||
name: 'Score',
|
||||
symbol: 'circle',
|
||||
areaStyle:
|
||||
{
|
||||
color:
|
||||
{
|
||||
type: 'radial',
|
||||
x: 0.5, y: 0.55, r: 0.5,
|
||||
colorStops:
|
||||
[
|
||||
{offset: 0, color: '#ffa0a0'},
|
||||
{offset: 0.5, color: '#fffead'},
|
||||
{offset: 1, color: '#d1ffde'}
|
||||
],
|
||||
global: false // 缺省为 false
|
||||
},
|
||||
opacity: 0.2
|
||||
},
|
||||
value: this.course.assignmentTypes.filter(t => t.graded).map(t => t.percent)
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
color: '#6771c1'
|
||||
};
|
||||
|
||||
return settings;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
<template>
|
||||
<div id="type-radar">
|
||||
<ve-radar height="420px" class="graph" :extend="{a: this.course.name}" :after-config="afterConfig"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./type-radar.ts" lang="ts"></script>
|
||||
@@ -1,116 +0,0 @@
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import Course from '@/logic/course';
|
||||
import {GPAUtils} from '@/logic/utils/gpa-utils';
|
||||
import Constants from '@/constants';
|
||||
import {FormatUtils} from '@/logic/utils/format-utils';
|
||||
|
||||
@Component
|
||||
export default class OverallBar extends Vue
|
||||
{
|
||||
@Prop({required: true}) courses: Course[];
|
||||
|
||||
/**
|
||||
* Generate settings
|
||||
*/
|
||||
get chartSettings()
|
||||
{
|
||||
let settings =
|
||||
{
|
||||
// Title
|
||||
title:
|
||||
{
|
||||
show: true,
|
||||
textStyle:
|
||||
{
|
||||
fontSize: 12
|
||||
},
|
||||
text: 'Course GPA',
|
||||
subtext: 'Current GPA for every course',
|
||||
x: 'center'
|
||||
},
|
||||
|
||||
// X axis represents course names
|
||||
xAxis:
|
||||
{
|
||||
type: 'category',
|
||||
axisLabel: {
|
||||
interval: 0,
|
||||
inside: false,
|
||||
rotate: 90,
|
||||
|
||||
// Truncate text length
|
||||
formatter: (value: string) => FormatUtils.limit(value, 16)
|
||||
},
|
||||
},
|
||||
|
||||
// Y axis represents GPAs and MaxGPAs
|
||||
yAxis:
|
||||
{
|
||||
type: 'value'
|
||||
},
|
||||
|
||||
// Data
|
||||
series:
|
||||
[
|
||||
// Max GP
|
||||
{
|
||||
type: 'bar',
|
||||
barGap: '-100%',
|
||||
data: this.courses.map(course =>
|
||||
{
|
||||
return {value: [course.name, GPAUtils.getGP(course, 'A+')], itemStyle: {color: '#d8d8d8'}}
|
||||
}),
|
||||
},
|
||||
// Current GP
|
||||
{
|
||||
type: 'bar',
|
||||
barGap: '-100%',
|
||||
data: this.generateGPData(),
|
||||
|
||||
label:
|
||||
{
|
||||
show: true,
|
||||
rotate: 90
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
// Disable tooltip
|
||||
tooltip:
|
||||
{
|
||||
show: false
|
||||
}
|
||||
};
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate GP data for each course
|
||||
*/
|
||||
private generateGPData()
|
||||
{
|
||||
let data: any = [];
|
||||
|
||||
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, gp],
|
||||
itemStyle:
|
||||
{
|
||||
color: Constants.THEME.colors[index]
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
<template>
|
||||
<div id="overall-bar">
|
||||
<ve-bar height="450px" class="graph" :extend="chartSettings"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./overall-bar.ts" lang="ts"></script>
|
||||
<style lang="scss" scoped>
|
||||
#overall-bar
|
||||
{
|
||||
.graph
|
||||
{
|
||||
margin-top: 50px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,152 +0,0 @@
|
||||
|
||||
// Row
|
||||
.assignment-entry
|
||||
{
|
||||
height: 40px;
|
||||
padding: 0 10px 0 20px;
|
||||
background: #f5f7fa;
|
||||
|
||||
text-align: left;
|
||||
|
||||
// Date
|
||||
.el-col.date
|
||||
{
|
||||
min-width: 150px;
|
||||
|
||||
span.month
|
||||
{
|
||||
margin-right: 5px;
|
||||
|
||||
// Unified width
|
||||
display: inline-block;
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
span.now
|
||||
{
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
}
|
||||
}
|
||||
|
||||
// Description
|
||||
.el-col.description
|
||||
{
|
||||
width: unset;
|
||||
|
||||
span.type
|
||||
{
|
||||
display: inline-block;
|
||||
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
|
||||
background: #eee;
|
||||
border-left: 2px solid #000;
|
||||
|
||||
height: 22px;
|
||||
line-height: 22px;
|
||||
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
// Grade
|
||||
.el-col.grade
|
||||
{
|
||||
text-align: right;
|
||||
float: right;
|
||||
|
||||
// Fix smaller screen display issues.
|
||||
width: unset;
|
||||
|
||||
// Status / Problems
|
||||
span.status
|
||||
{
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
// Percentage score
|
||||
span.percent
|
||||
{
|
||||
font-style: italic;
|
||||
background: #ffc;
|
||||
color: #555;
|
||||
|
||||
margin-right: 8px;
|
||||
|
||||
.symbol
|
||||
{
|
||||
padding: 0 1px;
|
||||
}
|
||||
}
|
||||
|
||||
// Score you got
|
||||
span.score
|
||||
{
|
||||
background: #f2f2f2;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
// Max score
|
||||
span.max
|
||||
{
|
||||
background: #ddd;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
// Mark as read
|
||||
button.mark-as-read
|
||||
{
|
||||
margin-left: 8px;
|
||||
color: #aaa;
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.entry-box
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
// Top shadow
|
||||
// https://stackoverflow.com/questions/17572619/inset-box-shadow-only-on-one-side
|
||||
box-shadow: inset 0 7px 9px -7px rgba(0,0,0,0.1);
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
<template>
|
||||
<div class="assignment-entry vertical-center"
|
||||
:class="narrow ? 'narrow' : ''"
|
||||
:style="`background: ${backgroundColor}`">
|
||||
|
||||
<el-row class="unread-row">
|
||||
<el-col :span="3" class="date">
|
||||
<span class="month">{{getMoment(assignment.time).format("MMM D")}}</span>
|
||||
<span class="now">({{getMoment(assignment.time).fromNow()}})</span>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="15" class="description">
|
||||
<span class="type entry-box"
|
||||
:style="`border-color: var(--assignment-type-${assignment.typeId})`">
|
||||
{{assignment.type}}
|
||||
</span>
|
||||
<span class="text">{{assignment.description}}</span>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="6" class="grade">
|
||||
<span v-if="assignment.problem" class="status entry-box" :style="{color: assignment.problemColor}">
|
||||
{{assignment.problem}}
|
||||
</span>
|
||||
<span v-if="assignment.graded" class="percent entry-box">
|
||||
{{(assignment.score / assignment.scoreMax * 100).toFixed(1)}}
|
||||
<span class="symbol">%</span>
|
||||
</span>
|
||||
<span v-if="assignment.graded" class="score entry-box">{{assignment.score}}</span>
|
||||
<span v-if="assignment.graded" class="max entry-box">{{assignment.scoreMax}}</span>
|
||||
|
||||
<el-button class="mark-as-read" :class="unread ? 'unread' : 'no-unread'"
|
||||
size="mini" type="text" icon="el-icon-close"
|
||||
@click="markAsRead">
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import moment from 'moment';
|
||||
import {Assignment} from '@/logic/course';
|
||||
|
||||
@Component
|
||||
export default class AssignmentEntry extends Vue
|
||||
{
|
||||
@Prop({required: true}) assignment: Assignment;
|
||||
|
||||
@Prop({default: false}) unread: boolean;
|
||||
@Prop({default: '#f5f7fa'}) backgroundColor: string;
|
||||
@Prop({default: false}) narrow: boolean;
|
||||
|
||||
/**
|
||||
* Format a date to the displayed format
|
||||
*
|
||||
* @param date Date
|
||||
*/
|
||||
getMoment(date: number)
|
||||
{
|
||||
return moment(new Date(date));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark this unread assignment as read
|
||||
*/
|
||||
markAsRead()
|
||||
{
|
||||
// Call custom event
|
||||
this.$emit('mark-as-read', this.assignment)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style src="./assignment-entry.scss" lang="scss" scoped/>
|
||||
@@ -1,216 +0,0 @@
|
||||
// Main card content
|
||||
.course-card-content.main
|
||||
{
|
||||
// Main color
|
||||
background: white;
|
||||
|
||||
// Alignment
|
||||
display: block;
|
||||
|
||||
padding: 20px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
#block-info
|
||||
{
|
||||
// Align left
|
||||
text-align: left;
|
||||
float: left;
|
||||
|
||||
#name
|
||||
{
|
||||
overflow: hidden;
|
||||
font-size: 22px;
|
||||
color: var(--main);
|
||||
}
|
||||
|
||||
#teacher
|
||||
{
|
||||
font-size: 12px;
|
||||
color: #999999;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
#block-grade
|
||||
{
|
||||
// Align right
|
||||
text-align: right;
|
||||
float: right;
|
||||
|
||||
// Adjust position
|
||||
margin-top: -2px;
|
||||
margin-left: 10px;
|
||||
|
||||
#grade
|
||||
{
|
||||
font-size: 21px;
|
||||
}
|
||||
|
||||
#updates
|
||||
{
|
||||
font-size: 14px;
|
||||
|
||||
#unread-number
|
||||
{
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
border-radius: 5px;
|
||||
|
||||
padding-left: 3px;
|
||||
padding-right: 3px;
|
||||
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
#unread-text
|
||||
{
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
#updates.unread
|
||||
{
|
||||
#unread-number
|
||||
{
|
||||
background: var(--unread);
|
||||
color: white;
|
||||
}
|
||||
|
||||
#unread-text
|
||||
{
|
||||
color: var(--unread);
|
||||
}
|
||||
}
|
||||
|
||||
#updates.none
|
||||
{
|
||||
color: #999999;
|
||||
#unread-number
|
||||
{
|
||||
background: #eeeeee;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#content
|
||||
{
|
||||
margin-top: -20px;
|
||||
padding: 20px 0 20px 20px;
|
||||
height: 50px;
|
||||
margin-left: -20px;
|
||||
}
|
||||
|
||||
#block-rate
|
||||
{
|
||||
// Align right
|
||||
width: 55px;
|
||||
float: right;
|
||||
|
||||
margin-left: 20px;
|
||||
padding: 0 10px;
|
||||
|
||||
color: white;
|
||||
background: #84c0ff;
|
||||
border-radius: 0 4px 4px 0;
|
||||
|
||||
height: 90px;
|
||||
margin-top: -20px;
|
||||
margin-right: -20px;
|
||||
|
||||
box-shadow: inset 8px 0 11px -4px rgba(0,0,0,.1);
|
||||
}
|
||||
|
||||
#block-rate.rated
|
||||
{
|
||||
background: #66eaad;
|
||||
}
|
||||
|
||||
.dark #block-rate > span
|
||||
{
|
||||
color: #84c0ff !important;
|
||||
}
|
||||
|
||||
.dark #block-rate.rated > span
|
||||
{
|
||||
color: #66eaad !important;
|
||||
}
|
||||
|
||||
#rating-popup
|
||||
{
|
||||
text-align: left;
|
||||
|
||||
.header
|
||||
{
|
||||
.title
|
||||
{
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.subtitle
|
||||
{
|
||||
margin-left: 10px;
|
||||
font-size: 12px;
|
||||
font-style: italic;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
|
||||
.item
|
||||
{
|
||||
margin-bottom: 10px;
|
||||
white-space: normal;
|
||||
|
||||
.title
|
||||
{
|
||||
font-size: 18px;
|
||||
color: var(--main);
|
||||
}
|
||||
|
||||
.description
|
||||
{
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.stars
|
||||
{
|
||||
font-size: 20px;
|
||||
color: #FFB300;
|
||||
}
|
||||
|
||||
.el-textarea
|
||||
{
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.item:first-child
|
||||
{
|
||||
margin-top: -15px;
|
||||
}
|
||||
}
|
||||
|
||||
#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,198 +0,0 @@
|
||||
<template>
|
||||
<div id="course-head" class="course-card-content main vertical-center">
|
||||
|
||||
<!-- Rating button -->
|
||||
<div id="block-rate" v-if="displayRate" class="vertical-center clickable"
|
||||
@click="ratingDialog = true" :class="{rated: course.rated}">
|
||||
<span v-if="!course.rated">Give a<br>Rating!</span>
|
||||
<span v-else>Rating<br>Entered</span>
|
||||
</div>
|
||||
|
||||
<!-- Main content -->
|
||||
<div id="content" @click="redirect" :class="clickable ? 'clickable' : ''">
|
||||
|
||||
<!-- Left -->
|
||||
<div id="block-info">
|
||||
<div id="name">{{course.name}}</div>
|
||||
<div id="teacher">{{course.teacherName}}</div>
|
||||
</div>
|
||||
|
||||
<!-- Right -->
|
||||
<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>
|
||||
|
||||
<!-- Rating Popup -->
|
||||
<el-dialog id="rating-popup" :visible.sync="ratingDialog" width="50%" top="5vh"
|
||||
:show-close="false" :close-on-click-modal="false" :close-on-press-escape="false">
|
||||
|
||||
<span slot="title" class="header">
|
||||
<div class="title">Give a Rating for {{course.name}}</div>
|
||||
<span class="subtitle">And for {{course.teacherName}}<br></span>
|
||||
<span class="subtitle" style="color: #e67b0d;">(might need to scroll down to find the submit button)</span>
|
||||
</span>
|
||||
|
||||
<div class="item" v-for="(criteria, index) of ratingCriteria">
|
||||
<div class="title">{{criteria.title}}</div>
|
||||
<div class="description">{{criteria.desc}}</div>
|
||||
|
||||
<div class="stars">
|
||||
<span class="star clickable" v-for="star in [0,1,2,3,4]" @click="changeStars(index, star)">
|
||||
<i v-if="rating.ratings[index] > star" class="el-icon-star-on"/>
|
||||
<i v-else class="el-icon-star-off"/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item">
|
||||
<div class="title">Comments</div>
|
||||
<div class="description">Any additional comments? (this is optional)</div>
|
||||
|
||||
<el-input type="textarea" placeholder="Comments... (Optional)"
|
||||
v-model="rating.comment" maxlength="4500" show-word-limit :autosize="{minRows: 2, maxRows: 4}">
|
||||
</el-input>
|
||||
<el-checkbox v-model="rating.anonymous">Anonymous</el-checkbox>
|
||||
</div>
|
||||
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="ratingDialog = false" :disabled="ratingPosting">Cancel</el-button>
|
||||
<el-button type="primary" @click="submitRating()" :disabled="canSubmit">
|
||||
{{course.rated ? 'Update' : 'Submit'}}
|
||||
</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import Course from '@/logic/course';
|
||||
import App from '@/components/app/app';
|
||||
import {GPAUtils} from '@/logic/utils/gpa-utils';
|
||||
import Constants from '@/constants';
|
||||
import {RATING_CRITERIA} from '@/logic/course-info';
|
||||
import Navigation from '@/components/navigation/navigation';
|
||||
|
||||
@Component
|
||||
export default class CourseHead extends Vue
|
||||
{
|
||||
@Prop({required: true}) unread: number;
|
||||
@Prop({required: true}) course: Course;
|
||||
@Prop({required: true}) clickable: boolean;
|
||||
|
||||
ratingDialog = false;
|
||||
ratingPosting = false;
|
||||
rating = this.course.rating;
|
||||
|
||||
get canSubmit()
|
||||
{
|
||||
return this.ratingPosting || App.instance.demoMode
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to the course page
|
||||
*/
|
||||
redirect()
|
||||
{
|
||||
if (!this.clickable) return;
|
||||
App.instance.nav.updateIndex(this.course.urlIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display rate button or not
|
||||
*/
|
||||
get displayRate()
|
||||
{
|
||||
return this.clickable && App.instance.showRating;
|
||||
}
|
||||
|
||||
get ratingCriteria() {return RATING_CRITERIA}
|
||||
|
||||
/**
|
||||
* Change star rating data
|
||||
*
|
||||
* @param index Index of the rating
|
||||
* @param star Change to how many stars
|
||||
*/
|
||||
changeStars(index: number, star: number)
|
||||
{
|
||||
this.rating.ratings[index] = star + 1;
|
||||
this.$forceUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit a rating
|
||||
*/
|
||||
submitRating()
|
||||
{
|
||||
if (this.rating.ratings.includes(0))
|
||||
{
|
||||
this.$message.error('You haven\'t rated all of the criteria yet!');
|
||||
return;
|
||||
}
|
||||
|
||||
this.ratingPosting = true;
|
||||
|
||||
App.http.post('/course-info/rating/set', {rating: this.rating}).then(response =>
|
||||
{
|
||||
if (response.success)
|
||||
{
|
||||
this.ratingDialog = false;
|
||||
this.ratingPosting = false;
|
||||
this.$message.success('Rating successfully posted, thank you!');
|
||||
|
||||
// First rating (Updating the first review doesn't count as first review)
|
||||
if (this.course.rated) return;
|
||||
this.course.rated = true;
|
||||
if (App.instance.courses.filter(c => c.rated).length == 1)
|
||||
{
|
||||
this.$alert('You can view other courses\'' +
|
||||
' ratings in the Course Selection tab, or review yours by clicking' +
|
||||
' the green button that says "Rating Entered." There are also option to turn off the rating buttons ' +
|
||||
' by clicking on your avatar on the top right corner.',
|
||||
'Thank you for submitting your fist rating!', {confirmButtonText: 'OK'}
|
||||
);
|
||||
}
|
||||
|
||||
// Last rating
|
||||
if (App.instance.courses.filter(c => c.isGraded && !c.rated).length == 0)
|
||||
{
|
||||
this.$confirm('You have rated all of your courses! Do you want to turn off the rating buttons?' +
|
||||
' (there are option to toggle them on again by clicking your avatar on the top right corner.)',
|
||||
'Thank you for submitting rating!',
|
||||
{confirmButtonText: 'Sure!', cancelButtonText: 'Nope.'}).then(() =>
|
||||
{
|
||||
// Disable rating buttons
|
||||
Navigation.instance.onAvatarMenu('switch-rating');
|
||||
this.$message.success('Rating buttons are disabled');
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this.$message.error('Sorry, but rating failed to post, please try again or email me if you continues to have issues. ' +
|
||||
'But wait! The email system isn\'t created yet... oops!. (Technical error message: ' + response.data + ')');
|
||||
this.ratingPosting = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style src="./course-head.scss" lang="scss" scoped/>
|
||||
@@ -1,16 +0,0 @@
|
||||
// Card
|
||||
.el-card.course-card
|
||||
{
|
||||
// Margins
|
||||
margin-right: 20px;
|
||||
margin-left: 20px;
|
||||
|
||||
// Height limit
|
||||
max-height: 250px;
|
||||
|
||||
// Limit name length
|
||||
white-space: nowrap;
|
||||
|
||||
// Expansion color
|
||||
background: #f4f6f9;
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
<template>
|
||||
<div id="overall-course">
|
||||
<el-card class="course-card">
|
||||
<course-head :clickable="true" :course="course" :unread="unread()"/>
|
||||
<div class="course-card-content expand"
|
||||
v-if="unread() !== 0">
|
||||
<assignment-entry v-for="assignment in unreadAssignments()"
|
||||
:assignment="assignment"
|
||||
:key="assignment.id"
|
||||
unread="true"
|
||||
v-on:mark-as-read="assignment.markAsRead()">
|
||||
</assignment-entry>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import AssignmentEntry from '@/pages/overall/overall-course/assignment-entry/assignment-entry.vue';
|
||||
import CourseHead from '@/pages/overall/overall-course/course-head/course-head.vue';
|
||||
import Course, {Assignment} from '@/logic/course';
|
||||
|
||||
@Component({
|
||||
components: {AssignmentEntry, CourseHead}
|
||||
})
|
||||
export default class OverallCourse extends Vue
|
||||
{
|
||||
@Prop({required: true}) course: Course;
|
||||
|
||||
mounted()
|
||||
{
|
||||
this.unreadAssignments().forEach(a => a.addCallback(() => this.$forceUpdate()));
|
||||
}
|
||||
|
||||
unreadAssignments(): Assignment[]
|
||||
{
|
||||
return this.course.assignments.filter(a => a.unread);
|
||||
}
|
||||
|
||||
unread(): number
|
||||
{
|
||||
return this.unreadAssignments().length;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style src="./overall-course.scss" lang="scss" scoped/>
|
||||
@@ -1,180 +0,0 @@
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import moment from 'moment';
|
||||
import Course from '@/logic/course';
|
||||
import {CourseUtils} from '@/logic/utils/course-utils';
|
||||
import GraphUtils from '@/logic/utils/graph-utils';
|
||||
import {GPAUtils} from '@/logic/utils/gpa-utils';
|
||||
import Constants from '@/constants';
|
||||
import Navigation from '@/components/navigation/navigation';
|
||||
|
||||
@Component
|
||||
export default class OverallLine extends Vue
|
||||
{
|
||||
@Prop({required: true}) courses: Course[];
|
||||
|
||||
filteredCourses: Course[];
|
||||
settings: any;
|
||||
|
||||
/**
|
||||
* When this component is created
|
||||
*/
|
||||
created()
|
||||
{
|
||||
// Filter courses
|
||||
this.filteredCourses = this.courses.filter(c => c.isGraded && c.assignments.length > 0);
|
||||
|
||||
// Generate settings
|
||||
this.settings =
|
||||
{
|
||||
...GraphUtils.getBaseSettings('Average Grade', 'Average score trend for every course'),
|
||||
|
||||
// Zoom bar
|
||||
dataZoom:
|
||||
[
|
||||
{
|
||||
type: 'slider',
|
||||
startValue: this.getStartDate(),
|
||||
|
||||
// Minimum zoom: 1 week
|
||||
minValueSpan: 7 * 24 * 60 * 60 * 1000
|
||||
}
|
||||
],
|
||||
|
||||
// Tooltip
|
||||
tooltip:
|
||||
{
|
||||
... GraphUtils.tooltipCssShadow(),
|
||||
|
||||
trigger: 'axis'
|
||||
},
|
||||
|
||||
// Axis
|
||||
xAxis:
|
||||
{
|
||||
type: 'time',
|
||||
axisLabel:
|
||||
{
|
||||
formatter: (name: any) => moment(name).format('MMM DD')
|
||||
},
|
||||
},
|
||||
yAxis:
|
||||
{
|
||||
axisLabel:
|
||||
{
|
||||
formatter: (name: any) => name + '%'
|
||||
},
|
||||
min: (value: any) => Math.floor(value.min),
|
||||
max: (value: any) => Math.min(Math.ceil(value.max), 110)
|
||||
},
|
||||
|
||||
// Series data
|
||||
series: this.series()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Override options
|
||||
*
|
||||
* @param options Original options (Unused)
|
||||
*/
|
||||
afterConfig(options: any)
|
||||
{
|
||||
return this.settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get starting date
|
||||
*/
|
||||
private getStartDate()
|
||||
{
|
||||
// If it's a past term, use the term's end date, else use today.
|
||||
let selected = Navigation.instance.getSelectedTerm();
|
||||
let end = selected == Constants.CURRENT_TERM || selected == -1
|
||||
? moment() : moment(CourseUtils.getTermEndDate());
|
||||
|
||||
return Math.max(end.subtract(30, 'days').toDate().getTime(),
|
||||
CourseUtils.getTermBeginDate().getTime())
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate series data
|
||||
*/
|
||||
private series()
|
||||
{
|
||||
// Each course
|
||||
let series: any[] = this.filteredCourses.map(course => this.getCourseSeries(course));
|
||||
|
||||
// Push other stuff
|
||||
series.push(
|
||||
{
|
||||
type: 'line',
|
||||
markLine: GraphUtils.getTermLines(),
|
||||
markArea: GraphUtils.getGradeMarkAreas(0.4)
|
||||
});
|
||||
|
||||
return series
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate series data for a course
|
||||
*
|
||||
* @param course
|
||||
*/
|
||||
private getCourseSeries(course: Course)
|
||||
{
|
||||
// Graded assignments
|
||||
let assignments = course.assignments.slice().reverse();
|
||||
|
||||
// Create series
|
||||
return {
|
||||
name: course.name,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'circle', // circle, diamond, emptyCircle, none
|
||||
data: this.toDateRange([...new Set(assignments.map(a => a.time))].map(time =>
|
||||
{
|
||||
// Find subset before this assignment
|
||||
let subset = course.getAssignmentsBefore(time);
|
||||
|
||||
// Find grade
|
||||
if (course.termGrading[subset.term].method == 'PERCENT_TYPE')
|
||||
return [time, GPAUtils.getPercentTypeAverage(course.termGrading[subset.term], subset.assignments)];
|
||||
else return [time, GPAUtils.getTotalMeanAverage(subset.assignments)];
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert point data to date range data.
|
||||
* Eg. [[Mon, 10], [Wed, 5]] to [[Mon, 10], [Tue, 10], [Wed, 5]]
|
||||
*
|
||||
* @param data
|
||||
*/
|
||||
private toDateRange(data: any[])
|
||||
{
|
||||
// Find the min date
|
||||
let minDate: Date = new Date(data[0][0]);
|
||||
|
||||
// Find the dates in between
|
||||
let now = new Date(Math.min(new Date().getTime(), CourseUtils.getTermEndDate().getTime()));
|
||||
let times: number[] = [];
|
||||
for (let date = minDate; date <= now; date.setDate(date.getDate() + 1)) times.push(date.getTime());
|
||||
|
||||
// Map the points
|
||||
let lastValue: any = null;
|
||||
return times.map(time =>
|
||||
{
|
||||
// Data point on this specific date
|
||||
let thisValue = data.find(a => a[0] == time);
|
||||
|
||||
// Switching terms
|
||||
if (Constants.TERMS.find(t => t.getTime() == time))
|
||||
lastValue = null;
|
||||
|
||||
// Find value
|
||||
return thisValue == null
|
||||
? lastValue == null ? null : [time, lastValue[1]]
|
||||
: [time, (lastValue = thisValue)[1]];
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
<template>
|
||||
<div id="overall-line">
|
||||
<ve-line :extend="{a: this.courses}" :after-config="afterConfig"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./overall-line.ts" lang="ts"></script>
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,60 +0,0 @@
|
||||
|
||||
.gpa-card
|
||||
{
|
||||
margin-left: 20px;
|
||||
min-width: 136px;
|
||||
}
|
||||
|
||||
.gpa
|
||||
{
|
||||
display: block;
|
||||
}
|
||||
|
||||
.gpa.header
|
||||
{
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.gpa.text
|
||||
{
|
||||
font-size: 35px;
|
||||
font-family: var(--font);
|
||||
}
|
||||
|
||||
.gpa.max
|
||||
{
|
||||
margin-top: -10px;
|
||||
margin-bottom: 10px;
|
||||
font-size: 12px;
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.gpa.time
|
||||
{
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.no-grade
|
||||
{
|
||||
font-size: 30px;
|
||||
color: #b1b1b1;
|
||||
|
||||
// Disable selecting
|
||||
display:block;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
// Cards
|
||||
.el-card.overall-bar-card
|
||||
{
|
||||
margin-right: 20px;
|
||||
min-width: 170px;
|
||||
}
|
||||
|
||||
.dialog-checkbox
|
||||
{
|
||||
display: block;
|
||||
margin-top: 20px;
|
||||
margin-bottom: -20px;
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
<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" @close="clearUnread(false)"
|
||||
width="30%" style="word-break: unset;">
|
||||
<span>You have too many new grade notifications. Clear them now?</span>
|
||||
<img src="./too-many-unread.png" alt=""/>
|
||||
<el-checkbox class="dialog-checkbox" v-model="dontAskAgain">Don't Ask Again</el-checkbox>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="clearUnread(false)" style="float: left">Nope</el-button>
|
||||
<el-button type="primary" @click="clearUnread(true)">Sure!</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
|
||||
<el-row v-if="getGPA().gpa !== -1">
|
||||
<el-col :span="4" class="overall-span">
|
||||
<el-card class="large gpa-card vertical-center" body-style="padding: 0">
|
||||
<div style="padding: 14px;">
|
||||
<span class="gpa header">GPA:</span>
|
||||
<span class="gpa text">{{getGPA().gpa}}</span>
|
||||
<span class="gpa max">(Out of {{getGPA().max}})</span>
|
||||
<div class="bottom clearfix gpa time">
|
||||
<time>{{ new Date().toDateString() }}</time>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="14" class="overall-span">
|
||||
<el-card class="large overall-line-card vertical-center" body-style="padding: 0 10px">
|
||||
<overall-line :courses="courses"/>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6" class="overall-span">
|
||||
<el-card class="large overall-bar-card vertical-center" body-style="padding: 0 10px">
|
||||
<overall-bar :courses="courses"/>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row v-if="getGPA().gpa === -1">
|
||||
<el-card class="large gpa-card vertical-center">
|
||||
<div class="no-grade">This quarter has no grades yet...</div>
|
||||
</el-card>
|
||||
</el-row>
|
||||
|
||||
<overall-course v-for="course in courses" :course="course" :key="course.id"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import OverallLine from '@/pages/overall/overall-line/overall-line';
|
||||
import OverallBar from '@/pages/overall/overall-bar/overall-bar';
|
||||
import OverallCourse from '@/pages/overall/overall-course/overall-course.vue';
|
||||
import Course, {Assignment} from '@/logic/course';
|
||||
import {GPAUtils} from '@/logic/utils/gpa-utils';
|
||||
|
||||
@Component({
|
||||
components: {OverallLine, OverallBar, OverallCourse}
|
||||
})
|
||||
export default class Overall extends Vue
|
||||
{
|
||||
@Prop({required: true}) courses: Course[];
|
||||
|
||||
/**
|
||||
* This function is called to get gpa since I can't import another
|
||||
* class in the Vue file.
|
||||
*/
|
||||
getGPA()
|
||||
{
|
||||
return GPAUtils.getGPA(this.courses);
|
||||
}
|
||||
|
||||
// For clear unread prompt
|
||||
unread: Assignment[];
|
||||
clearUnreadPrompt = false;
|
||||
dontAskAgain = false;
|
||||
started = false;
|
||||
|
||||
/**
|
||||
* Mark as read progress
|
||||
*/
|
||||
progress()
|
||||
{
|
||||
return +(this.unread.filter(a => !a.unread).length / this.unread.length * 100).toFixed(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* On page load - check if the user has too many notifications
|
||||
*/
|
||||
mounted()
|
||||
{
|
||||
// Check unread
|
||||
if (!this.$cookies.isKey('va.ignore-unread'))
|
||||
{
|
||||
// Count unread
|
||||
this.unread = this.courses.flatMap(c => c.assignments.filter(a => a.unread));
|
||||
|
||||
// Prompt clear
|
||||
if (this.unread.length > 15)
|
||||
{
|
||||
this.clearUnreadPrompt = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear unread
|
||||
*
|
||||
* @param confirmed
|
||||
*/
|
||||
clearUnread(confirmed: boolean)
|
||||
{
|
||||
// Hide prompt
|
||||
this.clearUnreadPrompt = false;
|
||||
|
||||
// Not confirmed, do nothing
|
||||
if (!confirmed)
|
||||
{
|
||||
if (!this.dontAskAgain) return;
|
||||
|
||||
// Don't ask again
|
||||
this.$cookies.set('va.ignore-unread', true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear unread
|
||||
this.started = true;
|
||||
this.unread.forEach((a, i) =>
|
||||
{
|
||||
// Delay: 100ms per assignment
|
||||
// I don't want my server to explode lol
|
||||
setTimeout(() => a.markAsRead().then(() => this.$forceUpdate()), 100 * i);
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style src="./overall.scss" lang="scss" scoped/>
|
||||
|
Before Width: | Height: | Size: 2.6 KiB |
@@ -1,13 +0,0 @@
|
||||
import Vue, {VNode} from 'vue';
|
||||
|
||||
declare global {
|
||||
namespace JSX {
|
||||
// tslint:disable no-empty-interface
|
||||
interface Element extends VNode {}
|
||||
// tslint:disable no-empty-interface
|
||||
interface ElementClass extends Vue {}
|
||||
interface IntrinsicElements {
|
||||
[elem: string]: any;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
declare module '*.vue'
|
||||
{
|
||||
import Vue from 'vue';
|
||||
export default Vue;
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
<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>
|
||||