Browse Source

refactor: Add an API layer package

Close #31
Sean Blackburn 1 năm trước cách đây
mục cha
commit
35fb217f2d
48 tập tin đã thay đổi với 568 bổ sung245 xóa
  1. 161 0
      package-lock.json
  2. 1 0
      package.json
  3. 5 0
      src/api/config.ts
  4. 17 0
      src/api/http.ts
  5. 10 0
      src/api/modules/changelog.ts
  6. 7 0
      src/api/modules/lecturers.ts
  7. 7 0
      src/api/modules/modules/history.ts
  8. 7 0
      src/api/modules/modules/index.ts
  9. 12 0
      src/api/modules/semesters/blockmodules.ts
  10. 12 0
      src/api/modules/semesters/changes.ts
  11. 12 0
      src/api/modules/semesters/config.ts
  12. 9 0
      src/api/modules/semesters/versions.ts
  13. 12 0
      src/api/modules/semesters/versions/classes.ts
  14. 12 0
      src/api/modules/semesters/versions/config.ts
  15. 5 0
      src/api/types/index.ts
  16. 5 0
      src/api/types/lecturers.ts
  17. 18 0
      src/api/types/modules/history.ts
  18. 23 0
      src/api/types/modules/index.ts
  19. 4 0
      src/api/types/semesters/changes.ts
  20. 3 0
      src/api/types/semesters/config.ts
  21. 4 0
      src/api/types/semesters/versions.ts
  22. 24 0
      src/api/types/semesters/versions/class.ts
  23. 5 0
      src/api/types/semesters/versions/config.ts
  24. 4 1
      src/components/general/ClassInfo.vue
  25. 2 2
      src/components/general/ClassRemovedRow.vue
  26. 2 1
      src/components/general/ClassRow.vue
  27. 1 1
      src/components/general/CurrentModuleExecutions.vue
  28. 1 1
      src/components/general/CurrentModuleExecutionsRow.vue
  29. 2 1
      src/components/general/DependencyTree.vue
  30. 3 1
      src/components/general/ModuleInfo.vue
  31. 8 6
      src/components/general/ModulesetManager.vue
  32. 1 1
      src/components/general/PreviousModuleExecutions.vue
  33. 1 1
      src/components/general/PreviousModuleExecutionsRow.vue
  34. 11 7
      src/components/modals/AdditionalInfo.vue
  35. 3 1
      src/components/modals/ClassModuleDetails.vue
  36. 7 6
      src/components/modals/ModuleSearchModal.vue
  37. 2 2
      src/components/views/ClassUpdateView.vue
  38. 4 3
      src/components/views/ModuleSelector.vue
  39. 15 13
      src/helpers.ts
  40. 26 19
      src/stores/ClassVersion.ts
  41. 50 43
      src/stores/classes.ts
  42. 3 3
      src/stores/config.ts
  43. 3 3
      src/stores/lecturers.ts
  44. 20 20
      src/stores/modules.ts
  45. 14 14
      src/stores/planning.ts
  46. 3 7
      src/stores/state.ts
  47. 3 3
      src/stores/upgrade.ts
  48. 4 85
      src/types.ts

+ 161 - 0
package-lock.json

@@ -12,6 +12,7 @@
         "@fortawesome/free-brands-svg-icons": "^6.6.0",
         "@fortawesome/free-solid-svg-icons": "^6.6.0",
         "@fortawesome/vue-fontawesome": "^3.0.8",
+        "axios": "^1.7.7",
         "leaflet": "^1.9.4",
         "mermaid": "^10.5.0",
         "pinia": "^2.2.2",
@@ -1594,6 +1595,12 @@
       "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
       "license": "Python-2.0"
     },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+      "license": "MIT"
+    },
     "node_modules/autoprefixer": {
       "version": "10.4.20",
       "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
@@ -1632,6 +1639,17 @@
         "postcss": "^8.1.0"
       }
     },
+    "node_modules/axios": {
+      "version": "1.7.7",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
+      "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
+      "license": "MIT",
+      "dependencies": {
+        "follow-redirects": "^1.15.6",
+        "form-data": "^4.0.0",
+        "proxy-from-env": "^1.1.0"
+      }
+    },
     "node_modules/balanced-match": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -1824,6 +1842,18 @@
       "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
       "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
     },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "license": "MIT",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
     "node_modules/commander": {
       "version": "7.2.0",
       "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
@@ -2402,6 +2432,15 @@
         "robust-predicates": "^3.0.0"
       }
     },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
     "node_modules/dequal": {
       "version": "2.0.3",
       "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@@ -2860,6 +2899,40 @@
       "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
       "license": "ISC"
     },
+    "node_modules/follow-redirects": {
+      "version": "1.15.9",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
+      "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/form-data": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+      "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+      "license": "MIT",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
     "node_modules/fraction.js": {
       "version": "4.3.7",
       "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
@@ -3739,6 +3812,27 @@
         "node": ">=8.6"
       }
     },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
     "node_modules/minimatch": {
       "version": "3.1.2",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -4193,6 +4287,12 @@
         "node": ">= 0.8.0"
       }
     },
+    "node_modules/proxy-from-env": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+      "license": "MIT"
+    },
     "node_modules/punycode": {
       "version": "2.3.1",
       "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -5887,6 +5987,11 @@
       "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
       "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
     },
+    "asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
+    },
     "autoprefixer": {
       "version": "10.4.20",
       "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
@@ -5901,6 +6006,16 @@
         "postcss-value-parser": "^4.2.0"
       }
     },
+    "axios": {
+      "version": "1.7.7",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
+      "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
+      "requires": {
+        "follow-redirects": "^1.15.6",
+        "form-data": "^4.0.0",
+        "proxy-from-env": "^1.1.0"
+      }
+    },
     "balanced-match": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -6018,6 +6133,14 @@
       "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
       "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
     },
+    "combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "requires": {
+        "delayed-stream": "~1.0.0"
+      }
+    },
     "commander": {
       "version": "7.2.0",
       "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
@@ -6457,6 +6580,11 @@
         "robust-predicates": "^3.0.0"
       }
     },
+    "delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
+    },
     "dequal": {
       "version": "2.0.3",
       "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@@ -6776,6 +6904,21 @@
       "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz",
       "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw=="
     },
+    "follow-redirects": {
+      "version": "1.15.9",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
+      "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="
+    },
+    "form-data": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+      "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+      "requires": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "mime-types": "^2.1.12"
+      }
+    },
     "fraction.js": {
       "version": "4.3.7",
       "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
@@ -7330,6 +7473,19 @@
         "picomatch": "^2.3.1"
       }
     },
+    "mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
+    },
+    "mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "requires": {
+        "mime-db": "1.52.0"
+      }
+    },
     "minimatch": {
       "version": "3.1.2",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -7602,6 +7758,11 @@
       "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
       "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="
     },
+    "proxy-from-env": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+    },
     "punycode": {
       "version": "2.3.1",
       "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",

+ 1 - 0
package.json

@@ -15,6 +15,7 @@
     "@fortawesome/free-brands-svg-icons": "^6.6.0",
     "@fortawesome/free-solid-svg-icons": "^6.6.0",
     "@fortawesome/vue-fontawesome": "^3.0.8",
+    "axios": "^1.7.7",
     "leaflet": "^1.9.4",
     "mermaid": "^10.5.0",
     "pinia": "^2.2.2",

+ 5 - 0
src/api/config.ts

@@ -0,0 +1,5 @@
+export const API_BASE_URL: string = "/data";
+export const DEFAULT_HEADERS: Record<string, string> = {
+  "Content-Type": "application/json",
+  Accept: "application/json",
+};

+ 17 - 0
src/api/http.ts

@@ -0,0 +1,17 @@
+import axios, { AxiosInstance } from "axios";
+import { API_BASE_URL, DEFAULT_HEADERS } from "./config";
+
+const httpRequest: AxiosInstance = axios.create({
+  baseURL: API_BASE_URL,
+  headers: DEFAULT_HEADERS,
+});
+
+httpRequest.interceptors.response.use(
+  (response) => response,
+  (error) => {
+    const message = error.response?.data?.message || "An error occurred";
+    return Promise.reject({ message, status: error.response?.status });
+  }
+);
+
+export { httpRequest };

+ 10 - 0
src/api/modules/changelog.ts

@@ -0,0 +1,10 @@
+import { httpRequest } from "../http";
+
+export const getChangelog = async (): Promise<string> => {
+  const response = await httpRequest.get<string>(`/changelog.html`, {
+    headers: {
+      accept: "text/html",
+    },
+  });
+  return response.data;
+};

+ 7 - 0
src/api/modules/lecturers.ts

@@ -0,0 +1,7 @@
+import { httpRequest } from "../http";
+import { Lecturer } from "../types/lecturers";
+
+export const getLecturers = async (): Promise<Lecturer[]> => {
+  const response = await httpRequest.get<Lecturer[]>(`/lecturers.json`);
+  return response.data;
+};

+ 7 - 0
src/api/modules/modules/history.ts

@@ -0,0 +1,7 @@
+import { httpRequest } from "../../http";
+import { ModuleHistory } from "../../types/modules/history";
+
+export const getModulesHistory = async (): Promise<ModuleHistory> => {
+  const response = await httpRequest.get<ModuleHistory>(`/module-history.json`);
+  return response.data;
+};

+ 7 - 0
src/api/modules/modules/index.ts

@@ -0,0 +1,7 @@
+import { httpRequest } from "../../http";
+import { Module } from "../../types/modules";
+
+export const getModules = async (): Promise<Module[]> => {
+  const response = await httpRequest.get<Module[]>(`/modules.json`);
+  return response.data;
+};

+ 12 - 0
src/api/modules/semesters/blockmodules.ts

@@ -0,0 +1,12 @@
+import { SemesterVersion } from "../../../types";
+import { httpRequest } from "../../http";
+import { TaughtClass } from "../../types/semesters/versions/class";
+
+export const getBlockmodules = async (
+  semver: SemesterVersion
+): Promise<TaughtClass[]> => {
+  const response = await httpRequest.get<TaughtClass[]>(
+    `${semver.semester}/blockclasses.json`
+  );
+  return response.data;
+};

+ 12 - 0
src/api/modules/semesters/changes.ts

@@ -0,0 +1,12 @@
+import { SemesterVersion } from "../../../types";
+import { httpRequest } from "../../http";
+import { TimetableChangelogEntry } from "../../types/semesters/changes";
+
+export const getSemesterChanges = async (
+  semver: SemesterVersion
+): Promise<TimetableChangelogEntry[]> => {
+  const response = await httpRequest.get<TimetableChangelogEntry[]>(
+    `${semver.semester}/changes.json`
+  );
+  return response.data;
+};

+ 12 - 0
src/api/modules/semesters/config.ts

@@ -0,0 +1,12 @@
+import { SemesterVersion } from "../../../types";
+import { httpRequest } from "../../http";
+import { SemesterConfiguration } from "../../types/semesters/config";
+
+export const getConfig = async (
+  semver: SemesterVersion
+): Promise<SemesterConfiguration> => {
+  const response = await httpRequest.get<SemesterConfiguration>(
+    `${semver.semester}/config.json`
+  );
+  return response.data;
+};

+ 9 - 0
src/api/modules/semesters/versions.ts

@@ -0,0 +1,9 @@
+import { httpRequest } from "../../http";
+import { SemesterVersions } from "../../types/semesters/versions";
+
+export const getSemesterVersions = async (): Promise<SemesterVersions[]> => {
+  const response = await httpRequest.get<SemesterVersions[]>(
+    `/semester-versions.json`
+  );
+  return response.data;
+};

+ 12 - 0
src/api/modules/semesters/versions/classes.ts

@@ -0,0 +1,12 @@
+import { SemesterVersion } from "../../../../types";
+import { httpRequest } from "../../../http";
+import { TaughtClass } from "../../../types/semesters/versions/class";
+
+export const getClasses = async (
+  semver: SemesterVersion
+): Promise<TaughtClass[]> => {
+  const response = await httpRequest.get<TaughtClass[]>(
+    `${semver.semester}/${semver.version}/classes.json`
+  );
+  return response.data;
+};

+ 12 - 0
src/api/modules/semesters/versions/config.ts

@@ -0,0 +1,12 @@
+import { SemesterVersion } from "../../../../types";
+import { httpRequest } from "../../../http";
+import { SemesterVersionConfig } from "../../../types/semesters/versions/config";
+
+export const getConfig = async (
+  semver: SemesterVersion
+): Promise<SemesterVersionConfig> => {
+  const response = await httpRequest.get<SemesterVersionConfig>(
+    `${semver.semester}/${semver.version}/config.json`
+  );
+  return response.data;
+};

+ 5 - 0
src/api/types/index.ts

@@ -0,0 +1,5 @@
+export interface ApiResponse<T> {
+  data: T;
+  message?: string;
+  status: number;
+}

+ 5 - 0
src/api/types/lecturers.ts

@@ -0,0 +1,5 @@
+export type Lecturer = {
+  short: string;
+  surname: string;
+  firstname: string;
+};

+ 18 - 0
src/api/types/modules/history.ts

@@ -0,0 +1,18 @@
+import { TeachingType } from "../../../types";
+
+export type HistoricClassEntry = {
+  semester: string;
+  class: string;
+  version: string;
+  lecturers: string[];
+  teaching_type: TeachingType;
+  rooms: string[];
+  from: number;
+  to: number;
+  id: string;
+  weekday: number;
+  pages: number[];
+  executionTime: string;
+};
+
+export type ModuleHistory = Record<string, HistoricClassEntry[]>;

+ 23 - 0
src/api/types/modules/index.ts

@@ -0,0 +1,23 @@
+export type Module = {
+  short: string;
+  name: string;
+  marks: string[] | null;
+  studiengang_id: number | null;
+  for_degrees: string | null;
+  module_id: number | null;
+  module_ids: number[] | null;
+  cat: string | null;
+  sub_cat: string | null;
+  ects: number | null;
+  full: string | null;
+  dependencies: Record<string, string[]>;
+  enablingModules: Record<string, string[]>;
+  hasMSP: boolean;
+  marksClean: string;
+  hasCompleted: boolean;
+  isActive: boolean;
+  hasFailed: boolean;
+  attemptCount: number;
+  maxAttemptsReached: boolean;
+  isContext: boolean;
+};

+ 4 - 0
src/api/types/semesters/changes.ts

@@ -0,0 +1,4 @@
+export type TimetableChangelogEntry = {
+  name: string;
+  changes: string;
+};

+ 3 - 0
src/api/types/semesters/config.ts

@@ -0,0 +1,3 @@
+export type SemesterConfiguration = {
+  blockclass_file: string;
+};

+ 4 - 0
src/api/types/semesters/versions.ts

@@ -0,0 +1,4 @@
+export type SemesterVersions = {
+  semester: string;
+  versions: string[];
+};

+ 24 - 0
src/api/types/semesters/versions/class.ts

@@ -0,0 +1,24 @@
+import { TeachingType } from "../../../../types";
+import { Module } from "../../modules";
+
+export type TaughtClass = {
+  id: string;
+  from: number;
+  to: number;
+  weekday: number | null;
+  class: string;
+  name: string;
+  rooms: string[];
+  teachers: string[];
+  teaching_type: TeachingType;
+  pages: number[];
+  degree_prg: string;
+  part_of_other_classes: string[];
+  module: Module | null;
+  isBB: boolean; // Berufsbegleitend, otherwise Full/Part time
+  isFirstPhase: boolean;
+  isEnglish: boolean;
+  isContext: boolean;
+  languageString: string;
+  executionTime: string;
+};

+ 5 - 0
src/api/types/semesters/versions/config.ts

@@ -0,0 +1,5 @@
+export type SemesterVersionConfig = {
+  export_date: string;
+  parse_date: string;
+  pdf_version: string;
+};

+ 4 - 1
src/components/general/ClassInfo.vue

@@ -198,10 +198,13 @@ import { useModulesStore } from "../../stores/modules";
 import { usePlanningStore } from "../../stores/planning";
 import { useStateStore } from "../../stores/state";
 import { useStudenthubStore } from "../../stores/studenthub";
-import { Module, TaughtClass, Lecturer, TeachingType } from "../../types";
+import { TeachingType } from "../../types";
 import CurrentModuleExecutions from "./CurrentModuleExecutions.vue";
 import TeachingTypeIcon from "./TeachingTypeIcon.vue";
 import { URLS, SCHOOL_NAME } from "../../globals";
+import { TaughtClass } from "../../api/types/semesters/versions/class";
+import { Lecturer } from "../../api/types/lecturers";
+import { Module } from "../../api/types/modules";
 
 export default {
   name: "ClassInfo",

+ 2 - 2
src/components/general/ClassRemovedRow.vue

@@ -92,11 +92,11 @@
 
 <script lang="ts">
 import { PropType } from "vue";
-import { TaughtClass } from "../../types";
 import ClassRow from "../general/ClassRow.vue";
 import { useClassesStore } from "../../stores/classes";
 import { usePlanningStore } from "../../stores/planning";
 import TeachingTypeIcon from "./TeachingTypeIcon.vue";
+import { TaughtClass } from "../../api/types/semesters/versions/class";
 
 export default {
   name: "ClassRemovedRow",
@@ -116,7 +116,7 @@ export default {
     alternatives(): TaughtClass[] {
       const classesStore = useClassesStore();
       return classesStore.allClasses.filter(
-        (cls) => cls.name == this.taughtClass.name,
+        (cls) => cls.name == this.taughtClass.name
       );
     },
     hasAlternativeChosen(): boolean {

+ 2 - 1
src/components/general/ClassRow.vue

@@ -87,11 +87,12 @@ import {
   addRemoveClassTitle,
   rowStyling,
 } from "../../helpers";
-import { TaughtClass, ClassSelectorColumn, TeachingType } from "../../types";
+import { ClassSelectorColumn, TeachingType } from "../../types";
 import { PropType } from "vue";
 import { useStudenthubStore } from "../../stores/studenthub";
 import { useStateStore } from "../../stores/state";
 import TeachingTypeIcon from "./TeachingTypeIcon.vue";
+import { TaughtClass } from "../../api/types/semesters/versions/class";
 
 export default {
   name: "ClassRow",

+ 1 - 1
src/components/general/CurrentModuleExecutions.vue

@@ -26,8 +26,8 @@
 import { PropType } from "vue";
 import { addRemoveClassTitle, classesPDFLink } from "../../helpers";
 import { usePlanningStore } from "../../stores/planning";
-import { TaughtClass } from "../../types";
 import CurrentModuleExecutionsRow from "./CurrentModuleExecutionsRow.vue";
+import { TaughtClass } from "../../api/types/semesters/versions/class";
 
 export default {
   name: "CurrentModuleExecutions",

+ 1 - 1
src/components/general/CurrentModuleExecutionsRow.vue

@@ -66,8 +66,8 @@ import { PropType } from "vue";
 import { addRemoveClassTitle, classesPDFLink } from "../../helpers";
 import { usePlanningStore } from "../../stores/planning";
 import { useStateStore } from "../../stores/state";
-import { TaughtClass } from "../../types";
 import TeachingTypeIcon from "../general/TeachingTypeIcon.vue";
+import { TaughtClass } from "../../api/types/semesters/versions/class";
 
 export default {
   name: "CurrentModuleExecutionsRow",

+ 2 - 1
src/components/general/DependencyTree.vue

@@ -89,9 +89,10 @@
 import { PropType } from "vue";
 import { useModulesStore } from "../../stores/modules";
 import { useStateStore } from "../../stores/state";
-import { Module, ModuleLanguages } from "../../types";
+import { ModuleLanguages } from "../../types";
 import { rowStyling } from "../../helpers";
 import { SCHOOL_NAME } from "../../globals";
+import { Module } from "../../api/types/modules";
 
 function availableLanguages(module: Module): string[] {
   const deps = module.dependencies;

+ 3 - 1
src/components/general/ModuleInfo.vue

@@ -178,10 +178,12 @@ import { useModulesStore } from "../../stores/modules";
 import { usePlanningStore } from "../../stores/planning";
 import { useStateStore } from "../../stores/state";
 import { useStudenthubStore } from "../../stores/studenthub";
-import { HistoricClassEntry, Module, TaughtClass } from "../../types";
 import CurrentModuleExecutions from "./CurrentModuleExecutions.vue";
 import PreviousModuleExecutions from "./PreviousModuleExecutions.vue";
 import { URLS, SCHOOL_NAME } from "../../globals";
+import { Module } from "../../api/types/modules";
+import { TaughtClass } from "../../api/types/semesters/versions/class";
+import { HistoricClassEntry } from "../../api/types/modules/history";
 
 export default {
   name: "ModuleInfo",

+ 8 - 6
src/components/general/ModulesetManager.vue

@@ -14,7 +14,9 @@
         :key="plan.name"
         :value="plan.name"
       >
-        {{ plan.name }} ({{ parsePDFVersion(plan.planVersion).semester }})
+        {{ plan.name }} ({{
+          parsePDFVersion(plan.planVersion)?.semester ?? "semester unbekannt"
+        }})
       </option>
     </select>
 
@@ -121,7 +123,7 @@ export default {
     deletePlan() {
       if (
         !confirm(
-          `Zusammengestellten Plan '${this.planningStore.currentPlanName}' löschen?`,
+          `Zusammengestellten Plan '${this.planningStore.currentPlanName}' löschen?`
         )
       )
         return;
@@ -133,7 +135,7 @@ export default {
       const copy = copyToClipboard(shareURL);
       if (copy == null) {
         this.toast.error(
-          `Konnte die URL nicht in die Zwischenablage kopieren (${shareURL})`,
+          `Konnte die URL nicht in die Zwischenablage kopieren (${shareURL})`
         );
         return;
       }
@@ -141,14 +143,14 @@ export default {
       copy.then(
         () => {
           this.toast.success(
-            `Der Share-Link wurde in die Zwischenablage kopiert`,
+            `Der Share-Link wurde in die Zwischenablage kopiert`
           );
         },
         () => {
           this.toast.error(
-            `Konnte die URL nicht in die Zwischenablage kopieren (${shareURL})`,
+            `Konnte die URL nicht in die Zwischenablage kopieren (${shareURL})`
           );
-        },
+        }
       );
     },
     showSettings() {

+ 1 - 1
src/components/general/PreviousModuleExecutions.vue

@@ -24,8 +24,8 @@
 
 <script lang="ts">
 import { PropType } from "vue";
-import { HistoricClassEntry } from "../../types";
 import PreviousModuleExecutionsRow from "./PreviousModuleExecutionsRow.vue";
+import { HistoricClassEntry } from "../../api/types/modules/history";
 
 export default {
   name: "PreviousModuleExecutions",

+ 1 - 1
src/components/general/PreviousModuleExecutionsRow.vue

@@ -40,8 +40,8 @@
 <script lang="ts">
 import { PropType } from "vue";
 import { classesPDFLink } from "../../helpers";
-import { HistoricClassEntry } from "../../types";
 import TeachingTypeIcon from "../general/TeachingTypeIcon.vue";
+import { HistoricClassEntry } from "../../api/types/modules/history";
 
 export default {
   name: "PreviousModuleExecutionsRow",

+ 11 - 7
src/components/modals/AdditionalInfo.vue

@@ -243,8 +243,11 @@
 import RightDrawer from "./RightDrawer.vue";
 import { URLS } from "../../globals";
 import { useClassVersionStore } from "../../stores/ClassVersion";
-import { MainView, TimetableChangelogEntry } from "../../types";
+import { MainView } from "../../types";
 import { useStateStore } from "../../stores/state";
+import { TimetableChangelogEntry } from "../../api/types/semesters/changes";
+import { getSemesterChanges } from "../../api/modules/semesters/changes";
+import { getChangelog } from "../../api/modules/changelog";
 
 export default {
   name: "AdditionalInfo",
@@ -276,14 +279,15 @@ export default {
   },
   methods: {
     fetchChangelog() {
-      fetch("./data/changelog.html")
-        .then((resp) => resp.text())
-        .then((txt) => (this.changelogContent = txt));
+      getChangelog().then((txt) => (this.changelogContent = txt));
     },
     fetchTimetableChangelog() {
-      fetch(this.classVersionStore.semesterFolder + "/changes.json")
-        .then((resp) => resp.json())
-        .then((data) => (this.timetableChangelog = data));
+      const semVer = this.classVersionStore.semVer;
+      if (semVer === undefined) return;
+
+      getSemesterChanges(semVer).then(
+        (data) => (this.timetableChangelog = data)
+      );
     },
     toggleVisibility() {
       if (this.stateStore.inspectingClass)

+ 3 - 1
src/components/modals/ClassModuleDetails.vue

@@ -49,12 +49,14 @@
 
 <script lang="ts">
 import { useStateStore } from "../../stores/state";
-import { Module, ModuleLanguages, TaughtClass } from "../../types";
+import { ModuleLanguages } from "../../types";
 import ClassInfo from "../general/ClassInfo.vue";
 import ModuleInfo from "../general/ModuleInfo.vue";
 import DependencyTree from "../general/DependencyTree.vue";
 import BaseModal from "./BaseModal.vue";
 import { URLS } from "../../globals";
+import { TaughtClass } from "../../api/types/semesters/versions/class";
+import { Module } from "../../api/types/modules";
 
 export default {
   name: "ClassModuleDetails",

+ 7 - 6
src/components/modals/ModuleSearchModal.vue

@@ -61,11 +61,12 @@
 import { useStateStore } from "../../stores/state";
 import { useModulesStore } from "../../stores/modules";
 import RightDrawer from "./RightDrawer.vue";
-import { ClassSelectorColumn, FilterRule, Module } from "../../types";
+import { ClassSelectorColumn, FilterRule } from "../../types";
 import NumberedPagination from "../general/NumberedPagination.vue";
 import { useClassesStore } from "../../stores/classes";
 import { watch } from "vue";
 import ClassFilter from "../general/ClassFilter.vue";
+import { Module } from "../../api/types/modules";
 
 export default {
   name: "ModuleSearchModal",
@@ -107,14 +108,14 @@ export default {
     matchingModules(): Module[] {
       return this.modulesStore.getModulesMatching(
         this.moduleNameFilter.filterData.term,
-        this.degreeFilter.filterData.degree,
+        this.degreeFilter.filterData.degree
       );
     },
     paginatedModules() {
       const idx = this.currentPageIdx;
       return this.matchingModules.slice(
         idx * this.pageSize,
-        (idx + 1) * this.pageSize,
+        (idx + 1) * this.pageSize
       );
     },
     currentSearchAndDegreeTerm(): [string, string] {
@@ -145,15 +146,15 @@ export default {
         const sd = this.currentSearchAndDegreeTerm;
         this.moduleNameFilter.filterData.term = sd[0];
         this.degreeFilter.filterData.degree = sd[1];
-      },
+      }
     );
     watch(
       () => this.moduleNameFilter.filterData.term,
-      () => (this.currentPageIdx = 0),
+      () => (this.currentPageIdx = 0)
     );
     watch(
       () => this.degreeFilter.filterData.degree,
-      () => (this.currentPageIdx = 0),
+      () => (this.currentPageIdx = 0)
     );
   },
   methods: {

+ 2 - 2
src/components/views/ClassUpdateView.vue

@@ -160,9 +160,9 @@ export default {
       // Prevent from showing when on exactly the same version
       const semVerStr = semesterVersionString(
         latest.semester,
-        latest.versions[0],
+        latest.versions[0]
       );
-      if (samePDFVersion(semVerStr, this.classVersionStore.semVer)) {
+      if (samePDFVersion(semVerStr, this.classVersionStore.semVerString)) {
         this.cancel();
         return;
       }

+ 4 - 3
src/components/views/ModuleSelector.vue

@@ -127,7 +127,7 @@ import { classesPDFLink, dayMap, toTime } from "../../helpers";
 import { watch } from "vue";
 import LoadSaveView from "../general/ModulesetManager.vue";
 import OrderingControl from "../general/OrderingControl.vue";
-import { TaughtClass, ClassSelectorColumn, Ordering } from "../../types";
+import { ClassSelectorColumn, Ordering } from "../../types";
 import { useStateStore } from "../../stores/state";
 import { useStudenthubStore } from "../../stores/studenthub";
 import { useModulesStore } from "../../stores/modules";
@@ -135,6 +135,7 @@ import ClassFilter from "../general/ClassFilter.vue";
 import ClassRow from "../general/ClassRow.vue";
 import ModuleSearchModal from "../modals/ModuleSearchModal.vue";
 import NumberedPagination from "../general/NumberedPagination.vue";
+import { TaughtClass } from "../../api/types/semesters/versions/class";
 
 export default {
   name: "ModuleSelector",
@@ -180,14 +181,14 @@ export default {
       const idx = this.currentPageIdx;
       return this.filteredModules.slice(
         idx * this.pageSize,
-        (idx + 1) * this.pageSize,
+        (idx + 1) * this.pageSize
       );
     },
   },
   mounted() {
     watch(
       () => this.filteredModules,
-      () => (this.currentPageIdx = 0),
+      () => (this.currentPageIdx = 0)
     );
   },
   methods: {

+ 15 - 13
src/helpers.ts

@@ -1,11 +1,10 @@
+import { TaughtClass } from "./api/types/semesters/versions/class";
+import { Module } from "./api/types/modules";
+import { HistoricClassEntry } from "./api/types/modules/history";
+import { SemesterVersions } from "./api/types/semesters/versions";
 import { useClassVersionStore } from "./stores/ClassVersion";
 import { useClassesStore } from "./stores/classes";
-import {
-  HistoricClassEntry,
-  Module,
-  SemesterVersion,
-  TaughtClass,
-} from "./types";
+import { SemesterVersion } from "./types";
 
 export function toTime(t: number): string {
   const hours = Math.floor(t / 3600);
@@ -130,11 +129,9 @@ export const MinFullNameSearchLength = 4;
  * @param versionStr The version string that should be parsed
  * @returns {Semester, Version}
  */
-export function parsePDFVersion(versionStr: string | null): {
-  semester: string | null;
-  version: string | null;
-} {
-  if (versionStr == null) return { semester: null, version: null };
+export function parsePDFVersion(
+  versionStr: string
+): SemesterVersion | undefined {
   const extractReInternal = /(.*)\|(.*)/;
   const extractRePdf = /\/ (HS|FS)_(\d{2})_(.*)/;
 
@@ -146,7 +143,7 @@ export function parsePDFVersion(versionStr: string | null): {
   }
 
   const res = versionStr.match(extractRePdf);
-  if (res == null) return { semester: null, version: null };
+  if (res == null) return undefined;
 
   const semester = res[2] + res[1];
   const version = res[3];
@@ -157,9 +154,14 @@ export function samePDFVersion(
   vers1: string | null,
   vers2: string | null
 ): boolean {
+  if (vers1 == null && vers2 == null) return true; // Both are null, so the same
+  if (vers1 == null || vers2 == null) return false; // one of them is null
+
   const parsed1 = parsePDFVersion(vers1);
   const parsed2 = parsePDFVersion(vers2);
 
+  if (parsed1 === undefined || parsed2 === undefined) return false; // One failed to parse, can not compare this
+
   return (
     parsed1.semester == parsed2.semester && parsed1.version == parsed2.version
   );
@@ -173,7 +175,7 @@ export function semesterVersionString(
 }
 
 export function semesterVersionStringC(
-  semVer: SemesterVersion | null,
+  semVer: SemesterVersions | null,
   versionIdx = 0
 ): string | null {
   if (semVer == null) return null;

+ 26 - 19
src/stores/ClassVersion.ts

@@ -5,16 +5,18 @@ import {
   parsePDFVersion,
   semesterVersionString,
 } from "../helpers";
-import { SemesterVersion } from "../types";
+import { SemesterVersions } from "../api/types/semesters/versions";
 import { useToast } from "vue-toastification";
 import { useClassesStore } from "./classes";
 import { useConfigStore } from "./config";
 import { usePlanningStore } from "./planning";
+import { getSemesterVersions } from "../api/modules/semesters/versions";
+import { SemesterVersion } from "../types";
 const toast = useToast();
 
 function showUnknownVersionToast() {
   toast.error(
-    "Die ausgewählte Semester / Stundenplan-Version existiert nicht!",
+    "Die ausgewählte Semester / Stundenplan-Version existiert nicht!"
   );
 }
 
@@ -23,16 +25,16 @@ export const useClassVersionStore = defineStore("classVersion", {
     return {
       semester: null as null | string,
       version: null as null | string,
-      semesterVersions: [] as SemesterVersion[],
+      semesterVersions: [] as SemesterVersions[],
       ready: null as Promise<null> | null,
     };
   },
   getters: {
-    latestSemester(): SemesterVersion | null {
+    latestSemester(): SemesterVersions | null {
       if (this.semesterVersions.length == 0) return null;
 
       const currSem = this.semesterVersions.find(
-        (el) => el.semester == this.semester,
+        (el) => el.semester == this.semester
       );
       if (currSem == null) return null;
 
@@ -42,7 +44,7 @@ export const useClassVersionStore = defineStore("classVersion", {
       if (this.semesterVersions.length == 0) return false;
       return this.semester == this.semesterVersions[0].semester;
     },
-    latestOverall(): SemesterVersion | null {
+    latestOverall(): SemesterVersions | null {
       if (this.semesterVersions.length == 0) return null;
       return this.semesterVersions[0];
     },
@@ -54,7 +56,14 @@ export const useClassVersionStore = defineStore("classVersion", {
       if (this.semester == null || this.version == null) return null;
       return getSemVerFolder(this.semester, this.version);
     },
-    semVer(): string | null {
+    semVer(): SemesterVersion | undefined {
+      if (this.semester == null) return undefined;
+      return {
+        semester: this.semester,
+        version: this.version,
+      };
+    },
+    semVerString(): string | null {
       if (this.semester == null || this.version == null) return null;
       return semesterVersionString(this.semester, this.version);
     },
@@ -62,7 +71,7 @@ export const useClassVersionStore = defineStore("classVersion", {
       if (this.semesterVersions.length == 0) return true;
 
       const currSem = this.semesterVersions.find(
-        (el) => el.semester == this.semester,
+        (el) => el.semester == this.semester
       );
       if (currSem == null) return true;
 
@@ -85,8 +94,7 @@ export const useClassVersionStore = defineStore("classVersion", {
       if (this.ready != null) return this.ready;
 
       this.ready = new Promise((resolve, reject) => {
-        fetch(`./data/semester-versions.json`)
-          .then((response) => response.json())
+        getSemesterVersions()
           .then((data) => {
             this.semesterVersions = data;
             if (this.semester == null && this.version == null) {
@@ -100,7 +108,7 @@ export const useClassVersionStore = defineStore("classVersion", {
           .catch((error) => {
             console.error(error);
             toast.error(
-              "Die Stundenplan-Versionen konnten nicht geladen werden!",
+              "Die Stundenplan-Versionen konnten nicht geladen werden!"
             );
             this.semesterVersions = [];
             reject();
@@ -112,8 +120,7 @@ export const useClassVersionStore = defineStore("classVersion", {
     useFromPDFString(version: string): boolean {
       const pdfVersion = parsePDFVersion(version);
 
-      if (pdfVersion.semester == null || pdfVersion.version == null)
-        return false;
+      if (pdfVersion === undefined || pdfVersion.version == null) return false;
 
       if (
         pdfVersion.semester != this.semester ||
@@ -125,13 +132,13 @@ export const useClassVersionStore = defineStore("classVersion", {
     },
     hasSemesterVersion(
       semester: string | null,
-      version: string | null,
+      version: string | null
     ): boolean {
       if (semester == null || version == null) return false;
 
       return (
         this.semesterVersions.filter(
-          (sv) => sv.semester == semester && sv.versions.includes(version),
+          (sv) => sv.semester == semester && sv.versions.includes(version)
         ).length > 0
       );
     },
@@ -170,9 +177,9 @@ export const useClassVersionStore = defineStore("classVersion", {
     },
     semesterFolderFromSemVer(version: string): string {
       const pdfVersion = parsePDFVersion(version);
-      if (pdfVersion.semester == null) {
+      if (pdfVersion === undefined) {
         console.error(
-          `Failed to find the semester in the given string: ${version}`,
+          `Failed to find the semester in the given string: ${version}`
         );
         return "";
       }
@@ -181,9 +188,9 @@ export const useClassVersionStore = defineStore("classVersion", {
     },
     semVerFolderFromSemVer(version: string): string {
       const pdfVersion = parsePDFVersion(version);
-      if (pdfVersion.semester == null || pdfVersion.version == null) {
+      if (pdfVersion === undefined || pdfVersion.version == null) {
         console.error(
-          `Failed to find the semester in the given string: ${version}`,
+          `Failed to find the semester in the given string: ${version}`
         );
         return "";
       }

+ 50 - 43
src/stores/classes.ts

@@ -6,22 +6,26 @@ import {
   dayMap,
   englishClassRe,
   firstPhaseClassRe,
+  parsePDFVersion,
   toTime,
 } from "../helpers";
 import {
-  TaughtClass,
   ClassSelectorColumn,
   Ordering,
   FilterRule,
   ModuleLanguages,
   ClassesVersion,
-  SemesterConfiguration,
 } from "../types";
 import { useClassVersionStore } from "./ClassVersion";
 import { useModulesStore } from "./modules";
 import { usePlanningStore } from "./planning";
 import { useStateStore } from "./state";
 import { useStudenthubStore } from "./studenthub";
+import { getBlockmodules } from "../api/modules/semesters/blockmodules";
+import { TaughtClass } from "../api/types/semesters/versions/class";
+import { getClasses } from "../api/modules/semesters/versions/classes";
+import { getConfig } from "../api/modules/semesters/config";
+import { SemesterConfiguration } from "../api/types/semesters/config";
 
 // eslint-disable-next-line
 type SortingFunction = (o1: any, o2: any) => number;
@@ -106,7 +110,7 @@ export const useClassesStore = defineStore("classes", {
 
         if (column == ClassSelectorColumn.Time) {
           transformedRules[column].forEach((rule) =>
-            foundClasses.push(...filterTimeColumn(classes, rule)),
+            foundClasses.push(...filterTimeColumn(classes, rule))
           );
           classes = [...new Set(foundClasses)];
           return;
@@ -122,7 +126,7 @@ export const useClassesStore = defineStore("classes", {
             }
             didFilter = true;
             foundClasses.push(
-              ...classes.filter((c) => c.degree_prg == rule.filterData.degree),
+              ...classes.filter((c) => c.degree_prg == rule.filterData.degree)
             );
           });
 
@@ -142,8 +146,8 @@ export const useClassesStore = defineStore("classes", {
                   (c) =>
                     c.name.toLowerCase().includes(term) ||
                     (checkName &&
-                      c.module?.name.toLocaleLowerCase().includes(term)),
-                ),
+                      c.module?.name.toLocaleLowerCase().includes(term))
+                )
               );
             });
             break;
@@ -167,7 +171,7 @@ export const useClassesStore = defineStore("classes", {
               if (term.length == 0) return;
               didFilter = true;
               foundClasses.push(
-                ...classes.filter((c) => c.class.toLowerCase().includes(term)),
+                ...classes.filter((c) => c.class.toLowerCase().includes(term))
               );
             });
             break;
@@ -179,8 +183,8 @@ export const useClassesStore = defineStore("classes", {
               didFilter = true;
               foundClasses.push(
                 ...classes.filter((c) =>
-                  c.teachers.join().toLowerCase().includes(term),
-                ),
+                  c.teachers.join().toLowerCase().includes(term)
+                )
               );
             });
             break;
@@ -192,8 +196,8 @@ export const useClassesStore = defineStore("classes", {
               didFilter = true;
               foundClasses.push(
                 ...classes.filter(
-                  (c) => c.teaching_type == rule.filterData.teachingType,
-                ),
+                  (c) => c.teaching_type == rule.filterData.teachingType
+                )
               );
             });
             break;
@@ -213,7 +217,7 @@ export const useClassesStore = defineStore("classes", {
       return cd.degreePrograms;
     },
     currentData(): ClassesVersion | null {
-      const version = this.cvStore.semVer;
+      const version = this.cvStore.semVerString;
       if (version == null) return null;
       if (!(version in this.data)) {
         console.error(`The version ${version} has not yet been loaded!`);
@@ -225,9 +229,9 @@ export const useClassesStore = defineStore("classes", {
   },
 
   actions: {
-    fetchData(version: string | null = null): Promise<null> {
+    fetchData(version: string | null = null): Promise<null> | undefined {
       if (version == null) {
-        version = this.cvStore.semVer;
+        version = this.cvStore.semVerString;
 
         if (version == null) {
           // console.error("The current semester/version was unexpectedly null!");
@@ -239,9 +243,9 @@ export const useClassesStore = defineStore("classes", {
       if (version in this.data)
         return this.data[version].ready ?? new Promise((res, rej) => rej);
 
-      const semVer = version;
-      this.data[semVer] = {
-        version: semVer,
+      const semVerStr = version;
+      this.data[semVerStr] = {
+        version: semVerStr,
         ready: null,
         loaded: false,
         teachers: [],
@@ -254,13 +258,18 @@ export const useClassesStore = defineStore("classes", {
         config: { blockclass_file: "block.pdf" },
       };
 
-      const semVerFolder = this.cvStore.semVerFolderFromSemVer(semVer);
-      const semesterFolder = this.cvStore.semesterFolderFromSemVer(semVer);
+      const semVer = parsePDFVersion(semVerStr);
+      if (semVer === undefined) {
+        console.error(
+          `Failed to find the semester in the given string: ${version}`
+        );
+        return undefined;
+      }
 
-      this.data[semVer].ready = new Promise((resolve, reject) => {
-        const fe = fetch(`${semVerFolder}/classes.json`);
-        const bc = fetch(`${semesterFolder}/blockclasses.json`);
-        const sc = fetch(`${semesterFolder}/config.json`);
+      this.data[semVerStr].ready = new Promise((resolve, reject) => {
+        const fe = getClasses(semVer);
+        const bc = getBlockmodules(semVer);
+        const sc = getConfig(semVer);
         const modulesStore = useModulesStore();
 
         const addModuleGetters = (cls: TaughtClass) => {
@@ -290,15 +299,14 @@ export const useClassesStore = defineStore("classes", {
          */
         const bmReady = new Promise((resolveBM) => {
           Promise.all([bc, modulesStore.ready])
-            .then((response) => response[0].json())
-            .then((data: TaughtClass[]) => {
+            .then(([data]: [TaughtClass[], unknown]) => {
               data.forEach(addModuleGetters);
-              this.data[semVer].blockClasses = data;
+              this.data[semVerStr].blockClasses = data;
               resolveBM(null);
             })
             .catch((error) => {
               console.error(error);
-              this.data[semVer].blockClasses = [];
+              this.data[semVerStr].blockClasses = [];
               resolveBM(null); // We will resolve and not reject, as it's okay to not have a blockmodules.json on the server
             });
         });
@@ -308,15 +316,14 @@ export const useClassesStore = defineStore("classes", {
          */
         const scReady = new Promise((resolveSC) => {
           Promise.all([sc, modulesStore.ready])
-            .then((response) => response[0].json())
-            .then((data: SemesterConfiguration) => {
-              this.data[semVer].config = data;
+            .then(([data]: [SemesterConfiguration, unknown]) => {
+              this.data[semVerStr].config = data;
               resolveSC(null);
             })
             .catch((error) => {
               console.error(error);
-              this.data[semVer].config = { blockclass_file: "block.pdf" };
-              resolveSC(null); // We will resolve and not reject, as it's okay to not have a blockmodules.json on the server
+              this.data[semVerStr].config = { blockclass_file: "block.pdf" };
+              resolveSC(null); // We will resolve and not reject, as it's okay to not have a config.json on the server
             });
         });
 
@@ -324,13 +331,12 @@ export const useClassesStore = defineStore("classes", {
          * Process the standard classes
          */
         Promise.all([fe, bmReady, scReady, modulesStore.ready])
-          .then((response) => (response[0] as Response).json())
-          .then((data: TaughtClass[]) => {
+          .then(([data]: [TaughtClass[], unknown, unknown, unknown]) => {
             data.forEach(addModuleGetters);
-            this.data[semVer].taughtClasses = data;
-            this.updateUniqueDegreePrograms(semVer);
+            this.data[semVerStr].taughtClasses = data;
+            this.updateUniqueDegreePrograms(semVerStr);
 
-            this.data[semVer].loaded = true;
+            this.data[semVerStr].loaded = true;
 
             const planningStore = usePlanningStore();
             planningStore.loadFromStorage();
@@ -338,7 +344,8 @@ export const useClassesStore = defineStore("classes", {
           })
           .catch((error) => {
             console.error(error);
-            this.data[semVer].taughtClasses = [];
+            // TODO
+            this.data[semVerStr].taughtClasses = [];
             reject(null);
           });
       });
@@ -354,7 +361,7 @@ export const useClassesStore = defineStore("classes", {
       });
 
       this.data[version].degreePrograms.sort((a, b) =>
-        a.toLowerCase().localeCompare(b.toLowerCase()),
+        a.toLowerCase().localeCompare(b.toLowerCase())
       );
     },
     getById(module_id: string): TaughtClass | null {
@@ -464,7 +471,7 @@ export const useClassesStore = defineStore("classes", {
 
 function filterTimeColumn(
   modules: TaughtClass[],
-  filterset: FilterRule,
+  filterset: FilterRule
 ): TaughtClass[] {
   const timeData = filterset.filterData as Record<string, number>;
   const start = timeData.startTime;
@@ -482,7 +489,7 @@ function filterTimeColumn(
       modules = modules.filter(
         (m) =>
           ((m.weekday ?? 0) == startingDay && m.from >= startingTime) ||
-          (m.weekday ?? 0) > startingDay,
+          (m.weekday ?? 0) > startingDay
       );
     }
   }
@@ -495,7 +502,7 @@ function filterTimeColumn(
         modules = modules.filter(
           (m) =>
             (m.weekday ?? 0) == startingDay &&
-            (endingTime == 0 || m.to <= endingTime),
+            (endingTime == 0 || m.to <= endingTime)
         );
       } else {
         aEnd *= -1;
@@ -508,7 +515,7 @@ function filterTimeColumn(
         (m) =>
           (m.weekday ?? 0) < endingDay ||
           ((m.weekday ?? 0) == endingDay &&
-            (endingTime == 0 || m.to <= endingTime)),
+            (endingTime == 0 || m.to <= endingTime))
       );
     }
   }

+ 3 - 3
src/stores/config.ts

@@ -1,17 +1,17 @@
 import { defineStore } from "pinia";
-import { Config } from "../types";
 import { useClassVersionStore } from "./ClassVersion";
+import { SemesterVersionConfig } from "../api/types/semesters/versions/config";
 
 const defaultConfig = {
   export_date: "",
   parse_date: "",
   pdf_version: "",
-} as Config;
+} as SemesterVersionConfig;
 
 export const useConfigStore = defineStore("config", {
   state: () => {
     return {
-      config: defaultConfig as Config,
+      config: defaultConfig as SemesterVersionConfig,
       ready: null as Promise<Response> | null,
     };
   },

+ 3 - 3
src/stores/lecturers.ts

@@ -1,5 +1,6 @@
 import { defineStore } from "pinia";
-import { Lecturer } from "../types";
+import { Lecturer } from "../api/types/lecturers";
+import { getLecturers } from "../api/modules/lecturers";
 
 export const useLecturersStore = defineStore("lecturers", {
   state: () => {
@@ -10,8 +11,7 @@ export const useLecturersStore = defineStore("lecturers", {
 
   actions: {
     fetchData() {
-      fetch("./data/lecturers.json")
-        .then((response) => response.json())
+      getLecturers()
         .then((data) => {
           this.data = data;
         })

+ 20 - 20
src/stores/modules.ts

@@ -6,25 +6,27 @@ import {
   MinFullNameSearchLength,
   toTime,
 } from "../helpers";
-import { Module, ModuleHistory as ModuleHistory } from "../types";
 import { useStudenthubStore } from "./studenthub";
+import { getModules } from "../api/modules/modules";
+import { Module } from "../api/types/modules";
+import { getModulesHistory } from "../api/modules/modules/history";
+import { ModuleHistory } from "../api/types/modules/history";
 
 export const useModulesStore = defineStore("modules", {
   state: () => {
     return {
       data: [] as Module[],
       history: {} as ModuleHistory,
-      ready: null as Promise<Response> | null,
+      ready: null as Promise<Module[]> | null,
     };
   },
 
   actions: {
     fetchData() {
-      this.ready = fetch("./data/modules.json");
+      this.ready = getModules();
       const shStore = useStudenthubStore();
 
       this.ready
-        .then((response) => response.json())
         .then((data: Module[]) => {
           data.forEach((mod) => {
             Object.defineProperty(mod, "hasMSP", {
@@ -48,7 +50,7 @@ export const useModulesStore = defineStore("modules", {
                             return "EN";
                           return null;
                         })
-                        .filter((el) => el),
+                        .filter((el) => el)
                     ),
                   ].join(", "))
                 );
@@ -61,7 +63,7 @@ export const useModulesStore = defineStore("modules", {
                   this._hasCompleted ||
                   (this._hasCompleted = matchesOneOf(
                     shStore.completedModuleIds,
-                    mod.module_ids ?? [],
+                    mod.module_ids ?? []
                   ))
                 );
               },
@@ -73,7 +75,7 @@ export const useModulesStore = defineStore("modules", {
                   this._isActive ||
                   (this._isActive = matchesOneOf(
                     shStore.activeModuleIds,
-                    mod.module_ids ?? [],
+                    mod.module_ids ?? []
                   ))
                 );
               },
@@ -85,7 +87,7 @@ export const useModulesStore = defineStore("modules", {
                   this._hasFailed ||
                   (this._hasFailed = matchesOneOf(
                     shStore.failedModuleIds,
-                    mod.module_ids ?? [],
+                    mod.module_ids ?? []
                   ))
                 );
               },
@@ -117,19 +119,17 @@ export const useModulesStore = defineStore("modules", {
           this.data = [];
         });
 
-      fetch("./data/module-history.json")
-        .then((response) => response.json())
-        .then((data: ModuleHistory) => {
-          Object.values(data).forEach((moduleEntry) => {
-            moduleEntry.forEach((el) => {
-              el.executionTime =
-                el.weekday !== null
-                  ? `${dayMap[el.weekday]}, ${toTime(el.from)}-${toTime(el.to)}`
-                  : "Blockmodul";
-            });
+      getModulesHistory().then((data) => {
+        Object.values(data).forEach((moduleEntry) => {
+          moduleEntry.forEach((el) => {
+            el.executionTime =
+              el.weekday !== null
+                ? `${dayMap[el.weekday]}, ${toTime(el.from)}-${toTime(el.to)}`
+                : "Blockmodul";
           });
-          this.history = data;
         });
+        this.history = data;
+      });
     },
     fromModuleShort(moduleShort: string): Module | null {
       const r = this.data.find((el) => {
@@ -147,7 +147,7 @@ export const useModulesStore = defineStore("modules", {
     },
     getModulesMatching(
       shortName: string | null | undefined,
-      degree: string | null | undefined,
+      degree: string | null | undefined
     ): Module[] {
       if (!shortName) return this.data;
 

+ 14 - 14
src/stores/planning.ts

@@ -2,8 +2,6 @@ import { defineStore } from "pinia";
 import { useToast } from "vue-toastification";
 import {
   CalendarEvent,
-  TaughtClass,
-  Module,
   PersonalEvent,
   PlanningDump,
   PlanEntry,
@@ -17,6 +15,8 @@ import {
   waitTimeAfterOldPlanReminder,
   waitTimeAfterUpgradeRequestModal,
 } from "../helpers";
+import { TaughtClass } from "../api/types/semesters/versions/class";
+import { Module } from "../api/types/modules";
 
 const MAX_MODULE_COUNT = 40;
 const toast = useToast();
@@ -214,7 +214,7 @@ export const usePlanningStore = defineStore("planning", {
         })
         .reduce(
           (sum, c) => sum + (c.module?.hasMSP ? c.module?.ects ?? 0 : 0),
-          0,
+          0
         );
     },
   },
@@ -251,7 +251,7 @@ export const usePlanningStore = defineStore("planning", {
             c.class == cls &&
             c.name == name &&
             `${c.weekday}` == weekday &&
-            c.rooms.join("_") == rooms,
+            c.rooms.join("_") == rooms
         );
 
         if (found.length == 0) {
@@ -270,7 +270,7 @@ export const usePlanningStore = defineStore("planning", {
           const name = key.replace("moduleset-", "");
           if (name.length > 0) {
             const data = JSON.parse(
-              localStorage[`moduleset-${name}`],
+              localStorage[`moduleset-${name}`]
             ) as PlanningDump;
             const version = data.version;
             this.allPlans.push({ name, planVersion: version });
@@ -289,7 +289,7 @@ export const usePlanningStore = defineStore("planning", {
     createPlan(name: string): boolean {
       if (this.allPlans.some((el) => el.name == name)) {
         toast.warning(
-          `Der Plan '${name}' existiert bereits. Wähle einen anderen Namen.`,
+          `Der Plan '${name}' existiert bereits. Wähle einen anderen Namen.`
         );
         return false;
       }
@@ -370,7 +370,7 @@ export const usePlanningStore = defineStore("planning", {
       classVersionStore.useFromPDFString(
         useSavedVersion
           ? data.version
-          : useClassVersionStore().semVer ?? data.version,
+          : useClassVersionStore().semVerString ?? data.version
       );
       modulesStore.fetchData();
 
@@ -494,7 +494,7 @@ export const usePlanningStore = defineStore("planning", {
     },
     copyPlan(
       copyName: string | null = null,
-      copyFrom: string | null = null,
+      copyFrom: string | null = null
     ): string | null {
       if (copyFrom == null) copyFrom = this.currentPlanName;
 
@@ -516,7 +516,7 @@ export const usePlanningStore = defineStore("planning", {
       if (localStorage[`moduleset-${copyFrom}`] == null) {
         // copyFrom name does not exist!
         toast.error(
-          "Die zu kopierende Modulauswahl wurde nicht im Speicher gefunden!",
+          "Die zu kopierende Modulauswahl wurde nicht im Speicher gefunden!"
         );
         return null;
       }
@@ -525,7 +525,7 @@ export const usePlanningStore = defineStore("planning", {
         localStorage[`moduleset-${copyFrom}`];
 
       const copyData = JSON.parse(
-        localStorage[`moduleset-${copyName}`],
+        localStorage[`moduleset-${copyName}`]
       ) as PlanningDump;
 
       this.allPlans.push({
@@ -549,7 +549,7 @@ export const usePlanningStore = defineStore("planning", {
       if (localStorage[`moduleset-${current}`] == null) {
         // Current name does not exist!
         toast.error(
-          "Die zu umzubenennende Modulauswahl wurde nicht im Speicher gefunden!",
+          "Die zu umzubenennende Modulauswahl wurde nicht im Speicher gefunden!"
         );
         return false;
       }
@@ -561,7 +561,7 @@ export const usePlanningStore = defineStore("planning", {
 
       const pIdx = this.allPlans.findIndex((el) => el.name == current);
       if (pIdx >= 0) {
-        const planVersion = useClassVersionStore().semVer ?? "";
+        const planVersion = useClassVersionStore().semVerString ?? "";
         this.allPlans.splice(pIdx, 1, { name: newName, planVersion });
       }
 
@@ -572,7 +572,7 @@ export const usePlanningStore = defineStore("planning", {
       if (!forceEmpty) modules = this.chosen.map((m) => m.id);
 
       const data = {
-        version: useClassVersionStore().semVer,
+        version: useClassVersionStore().semVerString,
         modules: modules,
         personalEvents: this.personalEvents,
         lastPlanRequestTimestamp: this.lastPlanRequestTimestamp,
@@ -603,7 +603,7 @@ export const usePlanningStore = defineStore("planning", {
     },
     updatePersonalEvent(
       oldEvent: PersonalEvent,
-      newEvent: PersonalEvent,
+      newEvent: PersonalEvent
     ): boolean {
       const idx = this.personalEvents.indexOf(oldEvent);
       if (idx == -1) return false;

+ 3 - 7
src/stores/state.ts

@@ -1,12 +1,8 @@
 import { defineStore } from "pinia";
-import {
-  TaughtClass,
-  Module,
-  MainView,
-  ColourScheme,
-  PersonalEvent,
-} from "../types";
+import { MainView, ColourScheme, PersonalEvent } from "../types";
 import { useStudenthubStore } from "./studenthub";
+import { TaughtClass } from "../api/types/semesters/versions/class";
+import { Module } from "../api/types/modules";
 
 const SELECTED_DEGREE_PROGRAM_KEY = "selectedDegreeProgram";
 const HIDE_COMPLETED_CLASSES_KEY = "hideCompletedClasses";

+ 3 - 3
src/stores/upgrade.ts

@@ -31,7 +31,7 @@ export const useUpgradeStore = defineStore("upgrade", {
     },
     getChangesToPlan(
       semester: string,
-      version: string,
+      version: string
     ): Promise<ClassUpgradeDifference> | null {
       const classesStore = useClassesStore();
       const classesVersionStore = useClassVersionStore();
@@ -40,11 +40,11 @@ export const useUpgradeStore = defineStore("upgrade", {
       planningStore.loadChosen();
 
       const newSemVer = semesterVersionString(semester, version);
-      const currSemVer = classesVersionStore.semVer;
+      const currSemVer = classesVersionStore.semVerString;
 
       if (currSemVer == null) {
         console.error(
-          "The current planning store does not have a version set!",
+          "The current planning store does not have a version set!"
         );
         return null;
       }

+ 4 - 85
src/types.ts

@@ -1,70 +1,9 @@
-export type TaughtClass = {
-  id: string;
-  from: number;
-  to: number;
-  weekday: number | null;
-  class: string;
-  name: string;
-  rooms: string[];
-  teachers: string[];
-  teaching_type: TeachingType;
-  pages: number[];
-  degree_prg: string;
-  part_of_other_classes: string[];
-  module: Module | null;
-  isBB: boolean; // Berufsbegleitend, otherwise Full/Part time
-  isFirstPhase: boolean;
-  isEnglish: boolean;
-  isContext: boolean;
-  languageString: string;
-  executionTime: string;
-};
-
-export type Module = {
-  short: string;
-  name: string;
-  marks: string[] | null;
-  studiengang_id: number | null;
-  for_degrees: string | null;
-  module_id: number | null;
-  module_ids: number[] | null;
-  cat: string | null;
-  sub_cat: string | null;
-  ects: number | null;
-  full: string | null;
-  dependencies: Record<string, string[]>;
-  enablingModules: Record<string, string[]>;
-  hasMSP: boolean;
-  marksClean: string;
-  hasCompleted: boolean;
-  isActive: boolean;
-  hasFailed: boolean;
-  attemptCount: number;
-  maxAttemptsReached: boolean;
-  isContext: boolean;
-};
+import { TaughtClass } from "./api/types/semesters/versions/class";
+import { SemesterConfiguration } from "./api/types/semesters/config";
 
-export type HistoricClassEntry = {
+export type SemesterVersion = {
   semester: string;
-  class: string;
-  version: string;
-  lecturers: string[];
-  teaching_type: TeachingType;
-  rooms: string[];
-  from: number;
-  to: number;
-  id: string;
-  weekday: number;
-  pages: number[];
-  executionTime: string;
-};
-
-export type ModuleHistory = Record<string, HistoricClassEntry[]>;
-
-export type Lecturer = {
-  short: string;
-  surname: string;
-  firstname: string;
+  version: string | null;
 };
 
 export type NameModule = {
@@ -86,12 +25,6 @@ export type CalendarEvent = {
   width: number;
 };
 
-export type Config = {
-  export_date: string;
-  parse_date: string;
-  pdf_version: string;
-};
-
 export type StudienjahrgangAnmeldung = {
   studienjahrgangAnmeldungId: number;
   studienjahrgangId: number;
@@ -192,16 +125,6 @@ export type FilterRule = {
   enabled: boolean;
 };
 
-export type SemesterVersion = {
-  semester: string;
-  versions: string[];
-};
-
-export type TimetableChangelogEntry = {
-  name: string;
-  changes: string;
-};
-
 export type PersonalEvent = {
   name: string;
   weekday: number;
@@ -240,10 +163,6 @@ export type ClassesVersion = {
   config: SemesterConfiguration;
 };
 
-export type SemesterConfiguration = {
-  blockclass_file: string;
-};
-
 export type ClassUpgradeDifference = {
   oldVersion: string;
   newVersion: string;