Compare commits
375 Commits
0.5.4.1391
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 97e6b9e42b | |||
| 1e2da25d58 | |||
| 66ba3430fd | |||
| 1abbce4daa | |||
| d8430a0885 | |||
| c6054648d3 | |||
| 74d3183da3 | |||
| cc9a35355d | |||
| 90e4e334c4 | |||
| b51ec78ff5 | |||
| 8ef9b152f0 | |||
| 08b8b31e47 | |||
| 1bec167be6 | |||
| 641778b169 | |||
| 73a651f3aa | |||
| e84911c25d | |||
| 467e036411 | |||
| c643eedbc8 | |||
| 542ab0a286 | |||
| d31173fb9d | |||
| a8b49f713b | |||
| c65a6fb7da | |||
| 17808a3c1a | |||
| cc046010ff | |||
| 0c5b993f20 | |||
| 1c63f795f4 | |||
| ba14fc80a9 | |||
| 6015c27e1b | |||
| f4ae9f4a8c | |||
| dad1cb0c3c | |||
| 608c7f4613 | |||
| 991153997b | |||
| e8bc3e695b | |||
| 98fabf382c | |||
| 833eec4833 | |||
| 7aa258bc97 | |||
| a4b6c50c0e | |||
| 47deca8859 | |||
| ca01ee743b | |||
| 27ca1d8bfe | |||
| 05a6abc1ba | |||
| 29ba8509bc | |||
| a290a3d93e | |||
| 4fe83d732a | |||
| 5f7a564b33 | |||
| 0de22d4f0d | |||
| 15149d1d07 | |||
| 5b54fc3b27 | |||
| beed3883a8 | |||
| 254c2690e5 | |||
| 2839304dc5 | |||
| 48b4b989b1 | |||
| 96306afecf | |||
| d267988b55 | |||
| 5780a6fc25 | |||
| 28eddf4aea | |||
| d8a7a700c4 | |||
| fc6b3331ac | |||
| 2210e6389f | |||
| ebf692e6a3 | |||
| 5422c1c45f | |||
| f5c63aa958 | |||
| 4b0bf16916 | |||
| b8a5172d70 | |||
| c4735e5603 | |||
| e601f6b57a | |||
| 272e7e5b98 | |||
| 5304c77548 | |||
| 2e57869c5f | |||
| cde51445a1 | |||
| 9b144b58bb | |||
| 1796bf8fb8 | |||
| c587cf7953 | |||
| 922e3911d8 | |||
| aadbaf0f75 | |||
| 4b95053651 | |||
| f66aaa09b0 | |||
| f7b352f595 | |||
| 3cfb888a5f | |||
| d9d0ea3168 | |||
| 408d62c09d | |||
| a4c61f3dd9 | |||
| f3321ff267 | |||
| 2ef242099b | |||
| 3497c277e3 | |||
| 74c47b8dd9 | |||
| c860ffd315 | |||
| 1b13f4abb2 | |||
| a8c882e343 | |||
| 99c51bb205 | |||
| 142e0da7c0 | |||
| 226297c810 | |||
| 13167fe050 | |||
| 9665b3a793 | |||
| 95111476f5 | |||
| fcfdd248b6 | |||
| df792a57c8 | |||
| 41e68d070d | |||
| 42231c79d9 | |||
| 41cc66a9f4 | |||
| 71dcf9e8c1 | |||
| 38232c3182 | |||
| 13288c6620 | |||
| 11b855109a | |||
| 4110b91b45 | |||
| c90c50a4b6 | |||
| ef5322ebd5 | |||
| e82ae7cb88 | |||
| 0726783465 | |||
| 8a7007c7b7 | |||
| 606732c987 | |||
| b664674a06 | |||
| 9b201d8475 | |||
| 66d553f6a7 | |||
| 94a1a0f108 | |||
| e3af901caf | |||
| ad5f5c3e39 | |||
| ddc6aacaf3 | |||
| a88164319b | |||
| 93aeab2c01 | |||
| 0be4bc6cb4 | |||
| d12984cb3a | |||
| 2fc8986bd8 | |||
| f8b8588413 | |||
| 3519ca6018 | |||
| cffc39dbfd | |||
| c75dc76526 | |||
| e6d5e35ff0 | |||
| cb05b8be96 | |||
| 8735237626 | |||
| 67a9dcc37f | |||
| 18825670e4 | |||
| 17dc6045b0 | |||
| c8ec7893e1 | |||
| 837fda814b | |||
| e306b91e37 | |||
| 50377cd9ee | |||
| 969336f203 | |||
| ae14eae711 | |||
| 5603831f5e | |||
| 760cfe2463 | |||
| 509243f874 | |||
| 311544b5aa | |||
| 1cf9ff157a | |||
| 8e54749e2e | |||
| 9adc78f287 | |||
| 6309a77b63 | |||
| 1eb8b5c137 | |||
| e78c5c8cef | |||
| 01662cfa8d | |||
| 1bda3ccd53 | |||
| d9896f0b48 | |||
| ac13da1dc6 | |||
| 20988fb35e | |||
| 5e6c0853c1 | |||
| 7a467d6346 | |||
| 505953753c | |||
| e4ddd12b7b | |||
| e1decbf7c2 | |||
| cdd1712b85 | |||
| 356259f0ec | |||
| 7928fe6598 | |||
| bcc8c8492f | |||
| d26f6618ec | |||
| af919e77e7 | |||
| c0aa07f736 | |||
| ae3e5c5092 | |||
| a2665d12f6 | |||
| 00edf57ebc | |||
| f1bd6799a7 | |||
| 4b12b44f8b | |||
| f9fc840c3c | |||
| bfe3c62db7 | |||
| aa7cc12876 | |||
| 9a2476b095 | |||
| 297fa9a4c6 | |||
| 6fbe0b2d40 | |||
| cae401ce55 | |||
| 8874888846 | |||
| 1f5a27a425 | |||
| 81a686b2ec | |||
| 19dd5b8d00 | |||
| 351b6da4d1 | |||
| 078b36eea6 | |||
| f003f0d48c | |||
| 616f0a7420 | |||
| 208d816c64 | |||
| 43cb9e384f | |||
| f28bd1851a | |||
| 4807a6d503 | |||
| 2e8e4b330f | |||
| ff433f2fec | |||
| 0f92254402 | |||
| 230b69356c | |||
| 30f1600063 | |||
| f5beddd68e | |||
| c66a3b5b11 | |||
| 0fd4d31d6b | |||
| 86f9008804 | |||
| 380fe19a20 | |||
| 9e6eb3c71e | |||
| 5ea2fcf592 | |||
| 7a5bf96916 | |||
| cb45759d5e | |||
| 2ce6cb9d0b | |||
| b6ddfba700 | |||
| 22d9e278c5 | |||
| ee06f557dc | |||
| 11b6455c00 | |||
| 6cbbfd9b46 | |||
| 1d31bfb5ae | |||
| 1d6db35c5c | |||
| 6412482c06 | |||
| cc039655ae | |||
| fef4612122 | |||
| 4e7c458b7f | |||
| dbb51e50bd | |||
| ab5caf9701 | |||
| 0aeeb2a933 | |||
| 4c8b7e6ad0 | |||
| c3c9e2c175 | |||
| d821be41df | |||
| 103a7a2fa4 | |||
| f17122d1ca | |||
| 131027d1d9 | |||
| be0851cdd1 | |||
| 8a430dccc4 | |||
| 79208f811a | |||
| 14884d0111 | |||
| ed727a7f26 | |||
| 10d25ff091 | |||
| 587f1d4a93 | |||
| 63612f4474 | |||
| 5e67b1a08f | |||
| 8e4557af18 | |||
| cc4cbe0b28 | |||
| f6f009083f | |||
| 2744eb97b6 | |||
| 3691f16d8c | |||
| c0d23770b5 | |||
| 2fb69a5992 | |||
| 52e664dd18 | |||
| d923b6ea02 | |||
| f153cb3fff | |||
| 2cff73862d | |||
| 316ea5428e | |||
| 45e397b6c4 | |||
| 74148e3782 | |||
| 91f50946b5 | |||
| 83fc54fcda | |||
| d1cfc69b5c | |||
| 012e2d5aea | |||
| 0a5f7d3be2 | |||
| eff37aac03 | |||
| 4e23bfb507 | |||
| d1b167c002 | |||
| 0b51713d95 | |||
| 0306585cf8 | |||
| fce32881e3 | |||
| bb79ed6e92 | |||
| 4f44423239 | |||
| d5f01c8ab0 | |||
| 276f9651a1 | |||
| 39b1fad44e | |||
| 237dd45cde | |||
| e4b6494046 | |||
| 2300cf6bf0 | |||
| 6fb2d6a5f5 | |||
| 0a5cd155e2 | |||
| 9b3d30959d | |||
| b8339a4e4c | |||
| ebc3dfb75b | |||
| 1293967512 | |||
| f0abfa16ed | |||
| f1885509d8 | |||
| cbfad69d22 | |||
| 533d788b6a | |||
| 48d1931368 | |||
| 808e8a5708 | |||
| 396edfef41 | |||
| 8cad524217 | |||
| e377f678c8 | |||
| bdaca5aeda | |||
| 7ca8554d7e | |||
| fda91fbfe3 | |||
| 259d4c3474 | |||
| 2a4a54d60b | |||
| 6d55ae8180 | |||
| deda2a194e | |||
| 7b1078dad2 | |||
| 70a46c0782 | |||
| 3f48a97668 | |||
| 5391ca7137 | |||
| 80d463aba8 | |||
| 3e3ab4fdab | |||
| 97ca5c5443 | |||
| 9cd3bbbd48 | |||
| d092ef2972 | |||
| b6182c3466 | |||
| 902cb66cc4 | |||
| bf690a941c | |||
| bc5a679bb6 | |||
| 2a3109577c | |||
| 5631ea2ac3 | |||
| b84dd14ced | |||
| 2899f6836b | |||
| 24c89ac381 | |||
| 87a3aed16d | |||
| cd14b2a768 | |||
| df79998990 | |||
| 25d0965696 | |||
| e886f981a0 | |||
| 81f66b65de | |||
| 61c934c360 | |||
| ed1a667e09 | |||
| 3f27db8c77 | |||
| b975c0332e | |||
| a0ae839dd3 | |||
| 43c7918854 | |||
| e16a95837c | |||
| 3d6254f2cc | |||
| 367dccfe95 | |||
| 7c564f9497 | |||
| 546ba16bb2 | |||
| a958fc6d9e | |||
| 3f7433f4da | |||
| 37ff3f5dbf | |||
| d20479e88d | |||
| f9bf5b81c9 | |||
| 43e1e9f0ed | |||
| e4357abcfd | |||
| 3e26965ad0 | |||
| 6035136358 | |||
| 1472bf4dbe | |||
| 2bae41eed1 | |||
| ad4bb875ae | |||
| 0a145ecdb5 | |||
| 95a7907d72 | |||
| f019d31f99 | |||
| 3d7fa6cdae | |||
| 7e5302c913 | |||
| 4cf64a0418 | |||
| c8289f5278 | |||
| ea93bc8d1a | |||
| b1df999efb | |||
| 320c7ab8ff | |||
| b706c439c8 | |||
| f4e11fcbcc | |||
| 3dea971055 | |||
| 252be18c2f | |||
| ba84861628 | |||
| d974fb24a0 | |||
| b05b145911 | |||
| 06bffb905e | |||
| 8ea8a3474f | |||
| 7609513a07 | |||
| 8b1ffb412c | |||
| a137466ed1 | |||
| 852d016203 | |||
| 49765498e7 | |||
| 02c2ddd7d8 | |||
| e643b65d98 | |||
| e3b011003d | |||
| b6b1a9362c | |||
| 522f2f803b | |||
| 4d7109fa6b | |||
| c31043a760 | |||
| fed0634a51 | |||
| d4f643b3f4 | |||
| 18b17cdd67 | |||
| 74dab07d3f | |||
| 91e52d2fc0 | |||
| 4ba6f4a613 | |||
| df68cc5927 | |||
| 4201d9aa4e |
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
#echo "Switch to production database settings"
|
||||
#exit
|
||||
echo "Switch to production database settings"
|
||||
exit
|
||||
|
||||
# abort on errors
|
||||
set -e
|
||||
@@ -13,13 +13,13 @@ npm run build
|
||||
cd dist
|
||||
|
||||
# if you are deploying to a custom domain
|
||||
echo 'vera.hydev.org' > CNAME
|
||||
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:HyDevelop/VeracrossAnalyzer.Client.git master:gh-pages
|
||||
git push -f git@github.com:Hykilpikonna/VeracrossAnalyzerDemo.git master:gh-pages
|
||||
|
||||
cd -
|
||||
|
||||
Generated
+1761
-1376
File diff suppressed because it is too large
Load Diff
+14
-14
@@ -8,29 +8,29 @@
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/chroma-js": "^1.4.3",
|
||||
"@types/md5": "^2.1.33",
|
||||
"@types/chroma-js": "^2.0.0",
|
||||
"@types/md5": "^2.2.0",
|
||||
"chroma-js": "^2.1.0",
|
||||
"core-js": "^2.6.10",
|
||||
"echarts": "^4.5.0",
|
||||
"element-ui": "^2.13.0",
|
||||
"echarts": "^4.8.0",
|
||||
"element-ui": "^2.13.2",
|
||||
"md5": "^2.2.1",
|
||||
"moment": "^2.24.0",
|
||||
"moment": "^2.27.0",
|
||||
"p-wait-for": "^3.1.0",
|
||||
"v-charts": "^1.19.0",
|
||||
"vue": "^2.6.10",
|
||||
"vue-class-component": "^7.0.2",
|
||||
"vue-cookies": "^1.5.13",
|
||||
"vue-property-decorator": "^8.3.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.1.1",
|
||||
"node-sass": "^4.13.0",
|
||||
"sass-loader": "^7.3.1",
|
||||
"typescript": "^3.7.3",
|
||||
"vue-template-compiler": "^2.6.10"
|
||||
"@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": {
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"success":true,"data":{"assignments":[{"score_id":5593330,"id":322150,"assignment_id":322150,"assignment_type_id":3,"assignment_type":"Quiz","assignment_type_sort_key":3,"assignment_description":"2.3 Open Notes","grading_period":"Quarter 1","assignment_date_long":"Wed, Sep 11","due_date_long":"Wed, Sep 11","due_date":"Sep 11","due_day":"Wed","_date":"09/11/2019","include_in_calculated_grade":1,"num_attachments":0,"num_criteria":0,"num_feedback":0,"maximum_score":16,"points_possible":16,"raw_score":"14","percent_grade":"8750%","completion_status_id":3,"completion_status":"Complete","is_unread":1.0,"is_notification":0,"is_problem":0,"display_grade":1,"display_score":1,"display_maximum_score":1,"display_percent_grade":1,"display_points_possible":1,"allow_student_feedback":0},{"score_id":5584935,"id":321649,"assignment_id":321649,"assignment_type_id":3,"assignment_type":"Quiz","assignment_type_sort_key":3,"assignment_description":"2.2 Open Notes","grading_period":"Quarter 1","assignment_date_long":"Mon, Sep 09","due_date_long":"Mon, Sep 09","due_date":"Sep 09","due_day":"Mon","_date":"09/09/2019","include_in_calculated_grade":1,"num_attachments":0,"num_criteria":0,"num_feedback":0,"maximum_score":10,"points_possible":10,"raw_score":"6","percent_grade":"6000%","completion_status_id":3,"completion_status":"Complete","is_notification":0,"is_problem":0,"display_grade":1,"display_score":1,"display_maximum_score":1,"display_percent_grade":1,"display_points_possible":1,"allow_student_feedback":0},{"score_id":5602940,"id":322723,"assignment_id":322723,"assignment_type_id":2,"assignment_type":"Homework","assignment_type_sort_key":2,"assignment_description":"Autobiography","grading_period":"Quarter 1","assignment_date_long":"Wed, Sep 04","due_date_long":"Thu, Sep 05","due_date":"Sep 05","due_day":"Thu","_date":"09/05/2019","include_in_calculated_grade":1,"num_attachments":0,"num_criteria":0,"num_feedback":0,"maximum_score":10,"points_possible":10,"raw_score":"","percent_grade":"0%","completion_status_id":0,"completion_status":"Pending","is_notification":0,"is_problem":0,"display_grade":1,"display_score":1,"display_maximum_score":1,"display_percent_grade":1,"display_points_possible":1,"allow_student_feedback":0}],"attachments":[],"criteria":[],"criteria_grade_scale_levels":[]}}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"success":true,"data":{"assignments":[],"attachments":[],"criteria":[],"criteria_grade_scale_levels":[]}}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"success":true,"data":{"assignments":[],"attachments":[],"criteria":[],"criteria_grade_scale_levels":[]}}
|
||||
@@ -0,0 +1 @@
|
||||
{"success":true,"data":{"assignments":[],"attachments":[],"criteria":[],"criteria_grade_scale_levels":[]}}
|
||||
@@ -0,0 +1 @@
|
||||
{"success":true,"data":{"assignments":[],"attachments":[],"criteria":[],"criteria_grade_scale_levels":[]}}
|
||||
@@ -0,0 +1 @@
|
||||
{"success":true,"data":{"assignments":[],"attachments":[],"criteria":[],"criteria_grade_scale_levels":[]}}
|
||||
@@ -0,0 +1,250 @@
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"user": {
|
||||
"id": 1,
|
||||
"schoolPersonPk": 109467,
|
||||
"username": "ygui21",
|
||||
"lastLogin": "Jun 5, 2020 1:28:51 PM",
|
||||
"firstLogin": "Dec 2, 2019 7:31:15 PM",
|
||||
"firstName": "Yijie",
|
||||
"lastName": "Gui",
|
||||
"graduationYear": 2021,
|
||||
"emails": "ygui21@stjohnsprep.org",
|
||||
"classes": "32451|32453|32458|32856|32872|32874|32878|32880|32882|32890|33070|33093|33121|33173|33337|33464|34174|34197|34199|34209"
|
||||
},
|
||||
"token": "Removed",
|
||||
"courses": [
|
||||
{
|
||||
"level": "H",
|
||||
"id_ci": 196,
|
||||
"rating": {
|
||||
"id_ci": 196,
|
||||
"id_user": 1,
|
||||
"userFullName": "Yijie]\u003d[Gui",
|
||||
"ratings": [
|
||||
5,
|
||||
5,
|
||||
3,
|
||||
5,
|
||||
4
|
||||
],
|
||||
"comment": "Mr. Crowell\u0027s selected books for this course are very interesting, and the essay topics are generally unique too, allowing us to express our creativity. His lectures are also very in-depth, strengthening our understanding of the ideas that the authors wanted to express. However, the style of the classroom might be boring for some people. For grading fairness, I think it is very fair, but his standards are a little bit too high because achieving a perfect score for an essay is very much impossible."
|
||||
},
|
||||
"name": "English 3 H",
|
||||
"teacherName": "Mr. Crowell",
|
||||
"id": 33337,
|
||||
"assignmentsId": 10934147,
|
||||
"letterGrade": "A+",
|
||||
"numericGrade": 97.14,
|
||||
"status": "active"
|
||||
},
|
||||
{
|
||||
"level": "AP",
|
||||
"id_ci": 19,
|
||||
"rating": {
|
||||
"id_ci": 19,
|
||||
"id_user": -1,
|
||||
"userFullName": "Anonymous]\u003d[Student",
|
||||
"ratings": [
|
||||
5,
|
||||
5,
|
||||
5,
|
||||
5,
|
||||
5
|
||||
],
|
||||
"comment": "Calculus made so much easier"
|
||||
},
|
||||
"name": "AP Calculus AB (Juniors)",
|
||||
"teacherName": "Ms. Dobrowolski",
|
||||
"id": 32453,
|
||||
"assignmentsId": 10934142,
|
||||
"letterGrade": "A+",
|
||||
"numericGrade": 100.0,
|
||||
"status": "active"
|
||||
},
|
||||
{
|
||||
"level": "AP",
|
||||
"id_ci": 251,
|
||||
"rating": {
|
||||
"id_ci": 251,
|
||||
"id_user": 1,
|
||||
"userFullName": "Yijie]\u003d[Gui",
|
||||
"ratings": [
|
||||
5,
|
||||
5,
|
||||
5,
|
||||
5,
|
||||
5
|
||||
],
|
||||
"comment": "Cations go meow"
|
||||
},
|
||||
"name": "AP Chemistry",
|
||||
"teacherName": "Ms. Stone",
|
||||
"id": 33464,
|
||||
"assignmentsId": 10934148,
|
||||
"letterGrade": "A+",
|
||||
"numericGrade": 100.0,
|
||||
"status": "active"
|
||||
},
|
||||
{
|
||||
"level": "AP",
|
||||
"id_ci": 22,
|
||||
"rating": {
|
||||
"id_ci": 22,
|
||||
"id_user": 1,
|
||||
"userFullName": "Yijie]\u003d[Gui",
|
||||
"ratings": [
|
||||
5,
|
||||
5,
|
||||
5,
|
||||
3,
|
||||
4
|
||||
],
|
||||
"comment": "Mr. Dankert can\u0027t really systematically teach, often he forgets to tell us something very important and then forgets that he forgot. The labs explained a lot of them very well."
|
||||
},
|
||||
"name": "AP Physics 1",
|
||||
"teacherName": "Mr. Dankert",
|
||||
"id": 32458,
|
||||
"assignmentsId": 10934143,
|
||||
"letterGrade": "A",
|
||||
"numericGrade": 95.67,
|
||||
"status": "active"
|
||||
},
|
||||
{
|
||||
"level": "AP",
|
||||
"id_ci": 18,
|
||||
"rating": {
|
||||
"id_ci": 18,
|
||||
"id_user": 1,
|
||||
"userFullName": "Yijie]\u003d[Gui",
|
||||
"ratings": [
|
||||
5,
|
||||
5,
|
||||
5,
|
||||
5,
|
||||
5
|
||||
],
|
||||
"comment": "Psychology is the best, and most valuable class I\u0027ve ever taken! Everything is just so relatable to my life!"
|
||||
},
|
||||
"name": "AP Psychology",
|
||||
"teacherName": "Mr. Emerson",
|
||||
"id": 32451,
|
||||
"assignmentsId": 10934141,
|
||||
"letterGrade": "A+",
|
||||
"numericGrade": 100.0,
|
||||
"status": "active"
|
||||
},
|
||||
{
|
||||
"level": "A",
|
||||
"id_ci": 98,
|
||||
"rating": {
|
||||
"id_ci": 98,
|
||||
"id_user": 1,
|
||||
"userFullName": "Yijie]\u003d[Gui",
|
||||
"ratings": [
|
||||
5,
|
||||
5,
|
||||
5,
|
||||
5,
|
||||
5
|
||||
],
|
||||
"comment": "Mr. Pynchon is very nice and encouraging."
|
||||
},
|
||||
"name": "US History A",
|
||||
"teacherName": "Mr. Pynchon",
|
||||
"id": 33093,
|
||||
"assignmentsId": 10941280,
|
||||
"letterGrade": "A+",
|
||||
"numericGrade": 99.39,
|
||||
"status": "active"
|
||||
},
|
||||
{
|
||||
"level": "H",
|
||||
"id_ci": 100,
|
||||
"name": "US History H",
|
||||
"teacherName": "Ms. Heath",
|
||||
"id": 33096,
|
||||
"assignmentsId": 10934144,
|
||||
"status": "past"
|
||||
},
|
||||
{
|
||||
"level": "A",
|
||||
"id_ci": 134,
|
||||
"name": "Relational Dynamics A",
|
||||
"teacherName": "Mr. Reinbold",
|
||||
"id": 33173,
|
||||
"assignmentsId": 10934146,
|
||||
"status": "past"
|
||||
},
|
||||
{
|
||||
"level": "A",
|
||||
"id_ci": 77,
|
||||
"rating": {
|
||||
"id_ci": 77,
|
||||
"id_user": -1,
|
||||
"userFullName": "Anonymous]\u003d[Student",
|
||||
"ratings": [
|
||||
5,
|
||||
4,
|
||||
5,
|
||||
5,
|
||||
3
|
||||
],
|
||||
"comment": "Honestly, everything is great except that the grading is way too harsh for an Accelerated course."
|
||||
},
|
||||
"name": "Social Justice A",
|
||||
"teacherName": "Mr. Reinbold",
|
||||
"id": 33121,
|
||||
"assignmentsId": 10934145,
|
||||
"letterGrade": "A+",
|
||||
"numericGrade": 97.0,
|
||||
"status": "active"
|
||||
},
|
||||
{
|
||||
"level": "Sport",
|
||||
"name": "Yoga AS",
|
||||
"teacherName": "Ms. Fanikos",
|
||||
"id": 33070,
|
||||
"assignmentsId": 10935189,
|
||||
"status": "past"
|
||||
},
|
||||
{
|
||||
"level": "Club",
|
||||
"id_ci": 316,
|
||||
"name": "HS Magic Trick Club 2019",
|
||||
"teacherName": "Mr. Reinbold",
|
||||
"id": 34174,
|
||||
"assignmentsId": 10949139,
|
||||
"status": "active"
|
||||
},
|
||||
{
|
||||
"level": "Club",
|
||||
"id_ci": 331,
|
||||
"name": "HS Science \u0026amp; Technology 2019",
|
||||
"teacherName": "Ms. Erwin",
|
||||
"id": 34197,
|
||||
"assignmentsId": 10952100,
|
||||
"status": "active"
|
||||
},
|
||||
{
|
||||
"level": "Club",
|
||||
"id_ci": 333,
|
||||
"name": "HS Computer Club 2019",
|
||||
"teacherName": "Mr. Gilmore",
|
||||
"id": 34199,
|
||||
"assignmentsId": 10951448,
|
||||
"status": "active"
|
||||
},
|
||||
{
|
||||
"level": "Club",
|
||||
"id_ci": 336,
|
||||
"name": "HS Chinese Ambassadors Club 2019",
|
||||
"teacherName": "Mrs. Mills",
|
||||
"id": 34209,
|
||||
"assignmentsId": 10953979,
|
||||
"status": "active"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
cd ../
|
||||
npm run serve
|
||||
@@ -0,0 +1,45 @@
|
||||
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,11 +1,16 @@
|
||||
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
|
||||
{
|
||||
font-family: var(--font);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-align: center;
|
||||
color: #2c3e50;
|
||||
padding-bottom: 100px;
|
||||
}
|
||||
|
||||
#app-content
|
||||
@@ -24,11 +29,6 @@
|
||||
--assignment-type-2: #3f991e;
|
||||
--assignment-type-3: #ff9900;
|
||||
--assignment-type-4: #b02b02;
|
||||
|
||||
//--font: 'Avenir', Helvetica, Arial, sans-serif;
|
||||
--font: -apple-system, Nunito Sans, Avenir, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
|
||||
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue,
|
||||
Hiragino Sans GB, Microsoft YaHei, WenQuanYi Micro Hei, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.dark
|
||||
@@ -145,3 +145,16 @@ div.el-card.course-card > div.el-card__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;
|
||||
}
|
||||
|
||||
+57
-39
@@ -1,6 +1,6 @@
|
||||
import {Component, Vue} from 'vue-property-decorator';
|
||||
import Login from '@/components/login/login';
|
||||
import Navigation from '@/components/navigation/navigation';
|
||||
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';
|
||||
@@ -11,15 +11,14 @@ 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},
|
||||
components: {Login, Navigation, Overall, Loading, CoursePage, Info, CourseSelection},
|
||||
})
|
||||
export default class App extends Vue
|
||||
{
|
||||
// Is the login panel shown
|
||||
showLogin: boolean = true;
|
||||
|
||||
// List of course that the student takes
|
||||
courses: Course[] = [];
|
||||
gradedCourses: Course[] = [];
|
||||
@@ -49,7 +48,16 @@ export default class App extends Vue
|
||||
staticPage: string = '';
|
||||
|
||||
// Dark mode
|
||||
darkMode: boolean = false;
|
||||
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.
|
||||
@@ -68,8 +76,17 @@ export default class App extends Vue
|
||||
this.staticPage = 'info';
|
||||
}
|
||||
|
||||
// Dark mode
|
||||
this.darkMode = this.$cookies.isKey('dark');
|
||||
// Default config
|
||||
if (!this.$cookies.isKey('show-rating'))
|
||||
{
|
||||
this.showRating = Constants.CURRENT_TERM == 3;
|
||||
}
|
||||
|
||||
// Demo
|
||||
if (this.demoMode)
|
||||
{
|
||||
AppDemo.loadDemo(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,37 +104,13 @@ export default class App extends Vue
|
||||
|
||||
// Store user
|
||||
this.user = user;
|
||||
this.courses = user.courses
|
||||
|
||||
// Assign user to http client
|
||||
App.http.user = user;
|
||||
|
||||
// Load data
|
||||
this.loadCoursesAfterLogin();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load courses data after logging in.
|
||||
*/
|
||||
loadCoursesAfterLogin()
|
||||
{
|
||||
// Show loading message
|
||||
this.logLoading('2. Loading courses...');
|
||||
|
||||
// Post request
|
||||
App.http.post('/courses', {}).then(response =>
|
||||
{
|
||||
// Check success
|
||||
if (response.success)
|
||||
{
|
||||
// Save courses
|
||||
this.courses = response.data.map((courseJson: any) => new Course(courseJson));
|
||||
|
||||
// Load assignments
|
||||
this.loadAssignments();
|
||||
}
|
||||
else throw new Error(response.data);
|
||||
})
|
||||
.catch(e => this.showError(`Error: Course data failed to load.\n(${e})`));
|
||||
// Load assignments
|
||||
this.loadAssignments();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -126,7 +119,7 @@ export default class App extends Vue
|
||||
loadAssignments()
|
||||
{
|
||||
// Show loading message
|
||||
this.logLoading('3. Loading assignments...');
|
||||
this.logLoading('1. Loading assignments...');
|
||||
|
||||
// Get assignments for all the courses
|
||||
this.courses.forEach(course =>
|
||||
@@ -161,7 +154,7 @@ export default class App extends Vue
|
||||
checkGradingAlgorithms()
|
||||
{
|
||||
// Show loading message
|
||||
this.logLoading('4. Checking grading algorithms...');
|
||||
this.logLoading('2. Checking grading algorithms...');
|
||||
|
||||
// Loop through all the courses
|
||||
for (const course of this.gradedCourses)
|
||||
@@ -190,7 +183,7 @@ export default class App extends Vue
|
||||
// This is because only percent_type can update over time
|
||||
if (course.termGrading[i].method == 'TOTAL_MEAN')
|
||||
{
|
||||
this.$cookies.set(cookieIndex, 'TOTAL_MEAN', 'd');
|
||||
this.$cookies.set(cookieIndex, 'TOTAL_MEAN', '3d');
|
||||
}
|
||||
}
|
||||
else throw new Error(resp.data);
|
||||
@@ -206,9 +199,34 @@ export default class App extends Vue
|
||||
|
||||
// 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
|
||||
*
|
||||
|
||||
+15
-10
@@ -1,21 +1,19 @@
|
||||
<template>
|
||||
<div id="app" class="theme-default">
|
||||
<div id="app-inner" v-if="staticPage === ''" :class="{dark: darkMode}">
|
||||
<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"
|
||||
:courses="gradedCourses"
|
||||
:user="user"
|
||||
:nav="nav"
|
||||
: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>
|
||||
<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"/>
|
||||
@@ -27,3 +25,10 @@
|
||||
|
||||
<script src="./app.ts" lang="ts"></script>
|
||||
<style src="./app.scss" lang="scss"/>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.padding
|
||||
{
|
||||
padding-bottom: 100px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<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>
|
||||
@@ -26,6 +26,8 @@ export default class Login extends Vue
|
||||
*/
|
||||
created()
|
||||
{
|
||||
// TODO: Check maintenance
|
||||
|
||||
// Check login cookies
|
||||
if (this.$cookies.isKey('va.token'))
|
||||
{
|
||||
@@ -33,9 +35,6 @@ export default class Login extends Vue
|
||||
if (this.needToUpdateCookies()) this.clearCookies();
|
||||
else
|
||||
{
|
||||
// Show loading
|
||||
this.disableInput = this.loading = true;
|
||||
|
||||
// Login with token
|
||||
this.login('/login/token', {token: this.$cookies.get('va.token')});
|
||||
}
|
||||
@@ -52,53 +51,52 @@ export default class Login extends Vue
|
||||
// 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, send the username and password to the server.
|
||||
* 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.
|
||||
*/
|
||||
onLoginClick()
|
||||
loginClick()
|
||||
{
|
||||
// Make login button loading
|
||||
this.loading = true;
|
||||
// Simple checks
|
||||
if (this.username == '')
|
||||
{
|
||||
this.error = 'Username cannot be blank 🤔';
|
||||
}
|
||||
|
||||
// Format it
|
||||
this.username = this.username.toLowerCase().replace(/ /g, '').replace(/@.*/g, '');
|
||||
|
||||
// Login
|
||||
this.login('/login', {username: this.username, password: this.password});
|
||||
// Actually login
|
||||
this.login('/login', {username: this.username, password: this.password})
|
||||
}
|
||||
|
||||
/**
|
||||
* Actually post the login request and process the response
|
||||
*
|
||||
* @param url Posting url
|
||||
* @param data Data to be posted
|
||||
* 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 =>
|
||||
App.http.post(url, data).then(response =>
|
||||
{
|
||||
// Check success
|
||||
if (response.success)
|
||||
{
|
||||
// Maintenance
|
||||
if (response.data.maintenance)
|
||||
{
|
||||
this.maintenance = response.data.maintenance;
|
||||
return;
|
||||
}
|
||||
|
||||
// Save token to cookies
|
||||
this.$cookies.set('va.token', response.data.user.token, '27d');
|
||||
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.user));
|
||||
this.$emit('login:user', new LoginUser(response.data));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -109,6 +107,7 @@ export default class Login extends Vue
|
||||
}
|
||||
|
||||
// Show error message & allow user to retry
|
||||
// TODO: Automatic report error
|
||||
this.error = response.data;
|
||||
this.disableInput = this.loading = false;
|
||||
}
|
||||
@@ -121,14 +120,6 @@ export default class Login extends Vue
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This is called when the user hits enter on the input boxes.
|
||||
*/
|
||||
onEnter()
|
||||
{
|
||||
this.onLoginClick();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cookies
|
||||
*/
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
placeholder="SJP Username (Eg. flast21)"
|
||||
:class="{'input-error': error !== ''}"
|
||||
v-if="!disableInput"
|
||||
@keyup.enter.native="onEnter">
|
||||
@keyup.enter.native="loginClick">
|
||||
</el-input>
|
||||
|
||||
<el-input v-model="password"
|
||||
@@ -18,12 +18,12 @@
|
||||
show-password=""
|
||||
:class="{'input-error': error !== ''}"
|
||||
v-if="!disableInput"
|
||||
@keyup.enter.native="onEnter">
|
||||
@keyup.enter.native="loginClick">
|
||||
</el-input>
|
||||
|
||||
<div class="el-form-item__error custom">{{error}}</div>
|
||||
|
||||
<el-button plain type="primary" @click="onLoginClick" :loading="loading">Login</el-button>
|
||||
<el-button plain type="primary" @click="loginClick" :loading="loading">Login</el-button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -147,3 +147,8 @@ footer
|
||||
// Cursor
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.el-submenu__title
|
||||
{
|
||||
padding-right: 5px !important;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ 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';
|
||||
import App from '@/components/app/app.ts';
|
||||
|
||||
/**
|
||||
* This component is the top navigation bar
|
||||
@@ -11,8 +11,8 @@ import App from '@/components/app/app';
|
||||
@Component
|
||||
export default class Navigation extends Vue
|
||||
{
|
||||
@Prop({required: true}) app: App;
|
||||
@Prop({required: true}) nav: NavController;
|
||||
@Prop({required: true}) courses: Course[];
|
||||
@Prop({required: true}) user: LoginUser;
|
||||
|
||||
private gradingPeriod: string = 'All Year';
|
||||
@@ -92,10 +92,10 @@ export default class Navigation extends Vue
|
||||
findCourse(courseId: string, indexOffset: number)
|
||||
{
|
||||
// Find current course index
|
||||
let courseIndex = this.courses.findIndex(c => c.id == +courseId);
|
||||
let courseIndex = this.app.gradedCourses.findIndex(c => c.id == +courseId);
|
||||
|
||||
// Find next course
|
||||
return this.courses[courseIndex + indexOffset];
|
||||
return this.app.gradedCourses[courseIndex + indexOffset];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -137,20 +137,29 @@ export default class Navigation extends Vue
|
||||
}
|
||||
case 'switch-dark':
|
||||
{
|
||||
App.instance.darkMode = !App.instance.darkMode;
|
||||
this.app.darkMode = !this.app.darkMode;
|
||||
|
||||
if (this.isDark()) this.$cookies.set('dark', true);
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isDark()
|
||||
{
|
||||
return App.instance.darkMode;
|
||||
}
|
||||
|
||||
get version() {return Constants.VERSION}
|
||||
}
|
||||
|
||||
@@ -13,11 +13,13 @@
|
||||
|
||||
<el-submenu index="">
|
||||
<template slot="title">Courses</template>
|
||||
<el-menu-item v-for="course in courses"
|
||||
<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">
|
||||
@@ -27,7 +29,7 @@
|
||||
<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" disabled>Term 4</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>
|
||||
@@ -38,9 +40,17 @@
|
||||
<el-avatar :src="user.avatarUrl"/>
|
||||
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item style="text-align: center">{{user.nickname}}</el-dropdown-item>
|
||||
<el-dropdown-item style="text-align: center">{{user.firstName}}</el-dropdown-item>
|
||||
|
||||
<el-dropdown-item icon="el-icon-sunrise" command="switch-dark" divided>{{!isDark() ? 'Dark Mode (Unfinished)' : 'Light Mode'}}</el-dropdown-item>
|
||||
<el-dropdown-item icon="el-icon-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>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,36 @@
|
||||
<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
-1
@@ -10,7 +10,7 @@ export default class Constants
|
||||
// static API_URL: string = 'http://localhost:24021/api';
|
||||
|
||||
/** Current version */
|
||||
static VERSION: string = '0.5.4.1391';
|
||||
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';
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
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?`}
|
||||
];
|
||||
+41
-28
@@ -6,6 +6,7 @@ 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.
|
||||
@@ -54,7 +55,9 @@ export class Assignment
|
||||
this.scoreMax = json.maximum_score;
|
||||
this.score = +json.raw_score;
|
||||
|
||||
this.gradingPeriod = +json.grading_period.replace('Quarter ', '') - 1;
|
||||
// 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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,7 +68,7 @@ export class Assignment
|
||||
// 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');
|
||||
return this.include && (this.complete == 'Complete' || this.complete == 'Late' || this.complete == 'Incomplete' || this.complete == 'Not Turned In');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,10 +81,12 @@ export class Assignment
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,9 +98,10 @@ export class Assignment
|
||||
switch (this.complete)
|
||||
{
|
||||
case 'Pending': return '#b1b1b1';
|
||||
case 'Not Turned In': return '#ff0036';
|
||||
case 'Incomplete': return '#ff7a2f';
|
||||
case 'NREQ': return '#41b141';
|
||||
case 'Late': return '#ff0036';
|
||||
case 'Late': return '#ff7a2f';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,6 +148,8 @@ export interface AssignmentType
|
||||
score: number
|
||||
percent: number
|
||||
assignmentCount: number
|
||||
|
||||
graded: boolean
|
||||
}
|
||||
|
||||
export interface Grading
|
||||
@@ -152,21 +160,24 @@ export interface Grading
|
||||
|
||||
export default class Course
|
||||
{
|
||||
id: number;
|
||||
assignmentsId: number;
|
||||
name: string;
|
||||
teacherName: string;
|
||||
status: string;
|
||||
rawAssignments: Assignment[];
|
||||
id: number
|
||||
id_ci: number
|
||||
assignmentsId: number
|
||||
name: string
|
||||
teacherName: string
|
||||
status: string
|
||||
rawAssignments: Assignment[]
|
||||
rating: CourseInfoRating
|
||||
rated: boolean
|
||||
|
||||
rawLetterGrade?: string;
|
||||
rawNumericGrade?: number;
|
||||
rawLetterGrade?: string
|
||||
rawNumericGrade?: number
|
||||
|
||||
level: string;
|
||||
scaleUp: number;
|
||||
level: string
|
||||
scaleUp: number
|
||||
|
||||
termGrading: Grading[];
|
||||
termAssignments: Assignment[][];
|
||||
termGrading: Grading[]
|
||||
termAssignments: Assignment[][]
|
||||
|
||||
cache: CacheUtils = new CacheUtils();
|
||||
|
||||
@@ -178,10 +189,13 @@ export default class Course
|
||||
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;
|
||||
@@ -193,14 +207,10 @@ export default class Course
|
||||
this.rawLetterGrade = undefined;
|
||||
}
|
||||
|
||||
// Level and scaleUp
|
||||
let level = CourseUtils.detectLevel(this.name);
|
||||
if (level != undefined)
|
||||
{
|
||||
this.level = level.level;
|
||||
this.scaleUp = level.scaleUp;
|
||||
}
|
||||
else this.level = 'Unknown';
|
||||
// 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);
|
||||
}
|
||||
@@ -220,7 +230,7 @@ export default class Course
|
||||
this.rawAssignments.sort((a, b) => b.time - a.time);
|
||||
|
||||
// Filter assignments into terms
|
||||
this.termAssignments = [[], [], [], []];
|
||||
this.termAssignments = [[], [], [], [], []];
|
||||
|
||||
// Loop through it by time order
|
||||
this.rawAssignments.forEach(a => this.termAssignments[a.gradingPeriod].push(a));
|
||||
@@ -379,11 +389,14 @@ export default class Course
|
||||
return types.map(type =>
|
||||
{
|
||||
// Get assignments of the type
|
||||
let typeAssignments = this.assignments.filter(a => a.graded && a.type == 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 = typeAssignments.reduce((sum, a) => sum + a.score, 0);
|
||||
let scoreMax = typeAssignments.reduce((sum, a) => sum + a.scoreMax, 0);
|
||||
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'
|
||||
@@ -392,7 +405,7 @@ export default class Course
|
||||
// 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}
|
||||
assignmentCount: typeAssignments.length, graded: gradedAssignments.length > 0}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
+31
-21
@@ -1,25 +1,31 @@
|
||||
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;
|
||||
nickname: string;
|
||||
graduationYear: string;
|
||||
groups: string;
|
||||
emails: string[];
|
||||
classes: string[];
|
||||
birthday: string;
|
||||
avatarUrl: string;
|
||||
token: string;
|
||||
id: number
|
||||
schoolPersonPk: number
|
||||
username: string
|
||||
lastLogin: Date
|
||||
firstLogin: Date
|
||||
firstName: string
|
||||
lastName: string
|
||||
graduationYear: number
|
||||
emails: string[]
|
||||
classes: string[]
|
||||
avatarUrl: string
|
||||
|
||||
constructor(json: any)
|
||||
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;
|
||||
@@ -27,14 +33,18 @@ export default class LoginUser
|
||||
this.firstLogin = new Date(json.firstLogin);
|
||||
this.firstName = json.firstName;
|
||||
this.lastName = json.lastName;
|
||||
this.nickname = json.nickname;
|
||||
this.graduationYear = json.graduationYear;
|
||||
this.groups = json.groups;
|
||||
this.graduationYear = +json.graduationYear;
|
||||
this.emails = json.emails.split('|').map((e: any) => e.toLowerCase().trim());
|
||||
this.classes = json.classes.split('|');
|
||||
this.birthday = json.birthday;
|
||||
this.avatarUrl = json.avatarUrl;
|
||||
this.token = json.token;
|
||||
|
||||
// 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 == '')
|
||||
|
||||
@@ -1,60 +1,18 @@
|
||||
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 UNKNOWN_COURSE_LIST = new Map();
|
||||
UNKNOWN_COURSE_LIST.set('Piano Masterclass', LEVEL_H);
|
||||
UNKNOWN_COURSE_LIST.set('Multivariable Calculus with Differential Equations', LEVEL_H);
|
||||
UNKNOWN_COURSE_LIST.set('Introduction to Algorithmic Thinking and Computational Technologies', LEVEL_A);
|
||||
UNKNOWN_COURSE_LIST.set('Ceramics 1', LEVEL_CP);
|
||||
UNKNOWN_COURSE_LIST.set('Ceramics 2', LEVEL_A);
|
||||
UNKNOWN_COURSE_LIST.set('Sculpture', LEVEL_CP);
|
||||
UNKNOWN_COURSE_LIST.set('Drawing', LEVEL_CP);
|
||||
UNKNOWN_COURSE_LIST.set('Painting', LEVEL_CP);
|
||||
const LEVEL_SPORT = {level: 'Sport', scaleUp: -1};
|
||||
const LEVEL_NONE = {level: 'None', scaleUp: -1};
|
||||
const LEVEL_UNKNOWN = {level: 'Unknown', scaleUp: -1};
|
||||
|
||||
export class CourseUtils
|
||||
{
|
||||
/**
|
||||
* Detect course level based on course name
|
||||
*
|
||||
* @param name Course name
|
||||
*/
|
||||
static detectLevel(name: string)
|
||||
{
|
||||
// Common ones
|
||||
if (name.startsWith('AP')) return LEVEL_AP;
|
||||
if (name.endsWith(' H')) return LEVEL_H;
|
||||
if (name.endsWith(' A')) return LEVEL_A;
|
||||
if (name.endsWith(' CP')) return LEVEL_CP;
|
||||
if (name.startsWith('HS ')) return LEVEL_CLUB;
|
||||
if (name.startsWith('MS ')) return LEVEL_CLUB;
|
||||
|
||||
// Uncommon ones
|
||||
let lower = name.toLowerCase();
|
||||
|
||||
if (name.startsWith('Pre-AP')) return LEVEL_AP;
|
||||
if (lower.endsWith(' acc')) return LEVEL_A;
|
||||
if (name.endsWith('H')) return LEVEL_H;
|
||||
if (name.endsWith('A')) return LEVEL_A;
|
||||
if (name.endsWith('CP')) return LEVEL_CP;
|
||||
|
||||
// Even more uncommon
|
||||
if (lower.includes('honors')) return LEVEL_H;
|
||||
if (lower.includes('accelerated')) return LEVEL_A;
|
||||
if (name.includes('Advanced')) return LEVEL_A;
|
||||
|
||||
// Unknown course list
|
||||
if (UNKNOWN_COURSE_LIST.has(name)) return UNKNOWN_COURSE_LIST.get(name);
|
||||
|
||||
// Really unknown
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the begin date of the selected term
|
||||
*/
|
||||
@@ -74,4 +32,65 @@ export class CourseUtils
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,3 +5,8 @@ export function findLastIndex<T>(array: T[], callback: (v: T) => boolean): numbe
|
||||
let result = arr2.findIndex(callback);
|
||||
return result == -1 ? -1 : arr2.length - result - 1;
|
||||
}
|
||||
|
||||
export function isNumeric(str: string)
|
||||
{
|
||||
return !isNaN(parseFloat(str)) && isFinite(+str);
|
||||
}
|
||||
|
||||
@@ -186,4 +186,47 @@ export class GPAUtils
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,4 +28,25 @@ export class HttpUtils
|
||||
.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)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
|
||||
#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;
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<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/>
|
||||
@@ -0,0 +1,269 @@
|
||||
<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>
|
||||
@@ -0,0 +1,16 @@
|
||||
.header
|
||||
{
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
font-size: 24px;
|
||||
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.divider
|
||||
{
|
||||
margin-top: 20px !important;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<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>
|
||||
@@ -0,0 +1,63 @@
|
||||
<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>
|
||||
@@ -3,7 +3,8 @@
|
||||
<el-card :body-style="{padding: '0px'}">
|
||||
<div id="type-info-card">
|
||||
<span id="type-name">{{type.name}}</span>
|
||||
<span id="type-average">Average: {{type.percent}}%</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"
|
||||
@@ -61,7 +62,7 @@
|
||||
float: left;
|
||||
}
|
||||
|
||||
#type-average
|
||||
.type-average
|
||||
{
|
||||
// Font
|
||||
font-size: 14px;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import Constants from '@/constants';
|
||||
import {FormatUtils} from '@/logic/utils/format-utils';
|
||||
import moment from 'moment';
|
||||
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
|
||||
@@ -26,6 +27,8 @@ export default class CourseScatter extends Vue
|
||||
*/
|
||||
get chartSettings()
|
||||
{
|
||||
let term = Navigation.instance.getSelectedTerm()
|
||||
|
||||
// Create settings
|
||||
let settings =
|
||||
{
|
||||
@@ -40,7 +43,9 @@ export default class CourseScatter extends Vue
|
||||
{
|
||||
formatter: (name: any) => moment(name).format('MMM DD')
|
||||
},
|
||||
max: new Date().getTime()
|
||||
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
|
||||
@@ -98,7 +103,7 @@ export default class CourseScatter extends Vue
|
||||
private series()
|
||||
{
|
||||
// Create scatter plots
|
||||
let series: any[] = this.course.assignmentTypes.map((type, i) =>
|
||||
let series: any[] = this.course.assignmentTypes.filter(t => t.graded).map((type, i) =>
|
||||
{
|
||||
return {
|
||||
type: 'scatter',
|
||||
|
||||
@@ -37,7 +37,7 @@ export default class TypePie extends Vue
|
||||
radius: ['40%', '60%'],
|
||||
center: ['50%', '55%'],
|
||||
label: GraphUtils.pieTextStyle(),
|
||||
data: this.course.assignmentTypes.map((t, i) => {return {
|
||||
data: this.course.assignmentTypes.filter(t => t.graded).map((t, i) => {return {
|
||||
value: t.weight,
|
||||
name: `${t.name}\n${t.weight}%`,
|
||||
itemStyle:
|
||||
|
||||
@@ -23,7 +23,7 @@ export default class TypeRadar extends Vue
|
||||
*/
|
||||
get chartSettings()
|
||||
{
|
||||
let min = this.course.assignmentTypes.reduce((min, t) => Math.min(min, t.percent), 100);
|
||||
let min = this.course.assignmentTypes.filter(t => t.graded).reduce((min, t) => Math.min(min, t.percent), 100);
|
||||
|
||||
// Create settings
|
||||
let settings =
|
||||
@@ -54,7 +54,7 @@ export default class TypeRadar extends Vue
|
||||
opacity: 0.4
|
||||
}
|
||||
},
|
||||
indicator: this.course.assignmentTypes.map((t, i) => {return {
|
||||
indicator: this.course.assignmentTypes.filter(t => t.graded).map((t, i) => {return {
|
||||
name: `${t.name}\n${t.percent}%`,
|
||||
max: 100,
|
||||
min: min - 30,
|
||||
@@ -89,7 +89,7 @@ export default class TypeRadar extends Vue
|
||||
},
|
||||
opacity: 0.2
|
||||
},
|
||||
value: this.course.assignmentTypes.map(t => t.percent)
|
||||
value: this.course.assignmentTypes.filter(t => t.graded).map(t => t.percent)
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -94,6 +94,106 @@
|
||||
}
|
||||
}
|
||||
|
||||
#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
|
||||
|
||||
@@ -1,27 +1,82 @@
|
||||
<template>
|
||||
<div id="course-head" class="course-card-content main vertical-center"
|
||||
:class="clickable ? 'clickable' : ''" @click="redirect">
|
||||
<div id="block-info">
|
||||
<div id="name">{{course.name}}</div>
|
||||
<div id="teacher">{{course.teacherName}}</div>
|
||||
<div 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>
|
||||
<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>
|
||||
|
||||
<!-- 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>
|
||||
<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>
|
||||
|
||||
<!-- 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>
|
||||
<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>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
@@ -29,16 +84,27 @@
|
||||
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
|
||||
*/
|
||||
@@ -47,6 +113,85 @@
|
||||
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>
|
||||
|
||||
|
||||
@@ -45,10 +45,7 @@
|
||||
</el-card>
|
||||
</el-row>
|
||||
|
||||
<overall-course v-for="course in courses"
|
||||
:course="course"
|
||||
:key="course.id">
|
||||
</overall-course>
|
||||
<overall-course v-for="course in courses" :course="course" :key="course.id"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user