diff --git a/package.json b/package.json
index ec4ab6a1..9701aa51 100644
--- a/package.json
+++ b/package.json
@@ -15,7 +15,8 @@
"sideEffects": [".css"],
"imports": {
"#openapi": "./staticfiles/generated/openapi/index.ts",
- "#core:*": "./core/static/webpack/*"
+ "#core:*": "./core/static/webpack/*",
+ "#pedagogy:*": "./pedagogy/static/webpack/*"
},
"devDependencies": {
"@babel/core": "^7.25.2",
diff --git a/pedagogy/static/webpack/pedagogy/guide-index.js b/pedagogy/static/webpack/pedagogy/guide-index.js
new file mode 100644
index 00000000..6740b935
--- /dev/null
+++ b/pedagogy/static/webpack/pedagogy/guide-index.js
@@ -0,0 +1,120 @@
+import { uvFetchUvList } from "#openapi";
+
+const pageDefault = 1;
+const pageSizeDefault = 100;
+
+document.addEventListener("alpine:init", () => {
+ Alpine.data("uv_search", () => ({
+ uvs: {
+ count: 0,
+ next: null,
+ previous: null,
+ results: [],
+ },
+ loading: false,
+ page: pageDefault,
+ // biome-ignore lint/style/useNamingConvention: api is in snake_case
+ page_size: pageSizeDefault,
+ search: "",
+ department: [],
+ // biome-ignore lint/style/useNamingConvention: api is in snake_case
+ credit_type: [],
+ semester: [],
+ // biome-ignore lint/style/useNamingConvention: api is in snake_case
+ to_change: [],
+ pushstate: History.PUSH,
+
+ update: undefined,
+
+ initializeArgs() {
+ const url = new URLSearchParams(window.location.search);
+ this.pushstate = History.REPLACE;
+
+ this.page = Number.parseInt(url.get("page")) || pageDefault;
+ this.page_size = Number.parseInt(url.get("page_size")) || pageSizeDefault;
+ this.search = url.get("search") || "";
+ this.department = url.getAll("department");
+ this.credit_type = url.getAll("credit_type");
+ /* The semester is easier to use on the backend as an enum (spring/autumn/both/none)
+ and easier to use on the frontend as an array ([spring, autumn]).
+ Thus there is some conversion involved when both communicate together */
+ this.semester = url.has("semester") ? url.get("semester").split("_AND_") : [];
+
+ this.update();
+ },
+
+ async init() {
+ this.update = Alpine.debounce(async () => {
+ /* Create the whole url before changing everything all at once */
+ const first = this.to_change.shift();
+ // biome-ignore lint/correctness/noUndeclaredVariables: defined in script.js
+ let url = updateQueryString(first.param, first.value, History.NONE);
+ for (const value of this.to_change) {
+ // biome-ignore lint/correctness/noUndeclaredVariables: defined in script.js
+ url = updateQueryString(value.param, value.value, History.NONE, url);
+ }
+ // biome-ignore lint/correctness/noUndeclaredVariables: defined in script.js
+ updateQueryString(first.param, first.value, this.pushstate, url);
+ await this.fetchData(); /* reload data on form change */
+ this.to_change = [];
+ this.pushstate = History.PUSH;
+ }, 50);
+
+ const searchParams = ["search", "department", "credit_type", "semester"];
+ const paginationParams = ["page", "page_size"];
+
+ for (const param of searchParams) {
+ this.$watch(param, () => {
+ if (this.pushstate !== History.PUSH) {
+ /* This means that we are doing a mass param edit */
+ return;
+ }
+ /* Reset pagination on search */
+ this.page = pageDefault;
+ this.page_size = pageSizeDefault;
+ });
+ }
+ for (const param of searchParams.concat(paginationParams)) {
+ this.$watch(param, (value) => {
+ this.to_change.push({ param: param, value: value });
+ this.update();
+ });
+ }
+ window.addEventListener("popstate", () => {
+ this.initializeArgs();
+ });
+ this.initializeArgs();
+ },
+
+ async fetchData() {
+ this.loading = true;
+ const args = {
+ // biome-ignore lint/style/useNamingConvention: api is in snake_case
+ page_size: this.page_size,
+ };
+ for (const [param, value] of new URL(
+ window.location.href,
+ ).searchParams.entries()) {
+ // Deal with array type params
+ if (["credit_type", "department", "semester"].includes(param)) {
+ if (args[param] === undefined) {
+ args[param] = [];
+ }
+ args[param].push(value);
+ } else {
+ args[param] = value;
+ }
+ }
+ this.uvs = (
+ await uvFetchUvList({
+ query: args,
+ })
+ ).data;
+ this.loading = false;
+ },
+
+ maxPage() {
+ return Math.ceil(this.uvs.count / this.page_size);
+ },
+ }));
+});
diff --git a/pedagogy/templates/pedagogy/guide.jinja b/pedagogy/templates/pedagogy/guide.jinja
index 7155748f..be9cce86 100644
--- a/pedagogy/templates/pedagogy/guide.jinja
+++ b/pedagogy/templates/pedagogy/guide.jinja
@@ -9,6 +9,10 @@
{% endblock %}
+{% block additional_js %}
+
+{% endblock %}
+
{% block head %}
{{ super() }}
@@ -113,105 +117,6 @@
- {{ paginate_alpine("page", "max_page()") }}
+ {{ paginate_alpine("page", "maxPage()") }}
-
{% endblock content %}
diff --git a/tsconfig.json b/tsconfig.json
index ab03d0bb..7a3c404d 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -10,7 +10,8 @@
"moduleResolution": "node",
"paths": {
"#openapi": ["./staticfiles/generated/openapi/index.ts"],
- "#core:*": ["./core/static/webpack/*"]
+ "#core:*": ["./core/static/webpack/*"],
+ "#pedagogy:*": ["./pedagogy/static/webpack/*"]
}
}
}