+
{
font-family: "Poppins", sans-serif;
}
-.chip_container {
+.chip-container {
overflow-x: auto;
}
-.checkbox container {
- display: flex;
- justify-content: flex-end;
-}
-
.subject-checkbox {
margin-top: -15px;
}
diff --git a/frontend/src/components/subject/subjectsview/SubjectsHeaderContainer.vue b/frontend/src/components/subject/subjectsview/header/SubjectsHeaderContainer.vue
similarity index 93%
rename from frontend/src/components/subject/subjectsview/SubjectsHeaderContainer.vue
rename to frontend/src/components/subject/subjectsview/header/SubjectsHeaderContainer.vue
index 6aa0eb17..14fe97fb 100644
--- a/frontend/src/components/subject/subjectsview/SubjectsHeaderContainer.vue
+++ b/frontend/src/components/subject/subjectsview/header/SubjectsHeaderContainer.vue
@@ -17,8 +17,8 @@
diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts
index b28fd7d0..c5c0f4ab 100644
--- a/frontend/src/i18n/locales/en.ts
+++ b/frontend/src/i18n/locales/en.ts
@@ -9,6 +9,12 @@ export default {
},
no: "no",
to: "to ",
+ no_capital: "No",
+ yes_capital: "Yes",
+ cancel: "Cancel",
+ confirm: "Confirm",
+ search: "Search",
+ add: "Add",
},
logo: "ghent-university-logo-white.png",
logout: "logout",
@@ -110,6 +116,7 @@ export default {
search: "Search",
userTable: {
name: "Name",
+ surname: "Surname",
uid: "UGent ID",
email: "Email",
isTeacher: "Is Teacher",
@@ -154,6 +161,25 @@ export default {
student_subjects: "Show student subjects",
no_subjects: "No subjects found.",
},
+
+ create_subject: {
+ error_snackbar: "There needs to be at least one teacher amongst the subject instructors.",
+ cancel_dialog: "Cancel subject creation?",
+ new_subject: "New subject",
+ title: "Title",
+ enter_title: "Enter title",
+ enter_title_hint: "Enter a valid title for the new subject",
+ assign_self: "Assign myself as instructor",
+ field_required: "This field is required",
+ field_length: "Title must be at least 3 characters long",
+ subject_instructors: "Subject instructors",
+ search_for_instructors: "Search for instructors",
+ email: "Subject email",
+ enter_email: "Enter email",
+ email_hint: "Field is optional",
+ email_invalid: "Provided email is invalid",
+ },
+
group: {
not_found: "Group not found",
not_found2: "No groups found",
diff --git a/frontend/src/i18n/locales/nl.ts b/frontend/src/i18n/locales/nl.ts
index 872bb494..c203f6ed 100644
--- a/frontend/src/i18n/locales/nl.ts
+++ b/frontend/src/i18n/locales/nl.ts
@@ -8,6 +8,12 @@ export default {
},
no: "geen",
to: "naar ",
+ no_capital: "Neen",
+ yes_capital: "Ja",
+ cancel: "Annuleren",
+ confirm: "Bevestigen",
+ search: "Zoeken",
+ add: "Toevoegen",
},
logo: "universiteit-gent-logo-white.png",
logout: "uitloggen",
@@ -112,6 +118,7 @@ export default {
search: "Zoeken",
userTable: {
name: "Naam",
+ surname: "Achternaam",
uid: "UGent ID",
email: "Email",
isTeacher: "Is Lesgever",
@@ -157,6 +164,26 @@ export default {
student_subjects: "Toon student vakken",
no_subjects: "Geen vakken gevonden.",
},
+
+ create_subject: {
+ error_snackbar:
+ "Er moet minstens één lesgever bij de geselecteerde vakverantwoordelijken aanwezig zijn.",
+ cancel_dialog: "Vak aanmaken annuleren?",
+ new_subject: "Nieuw vak",
+ title: "Titel",
+ enter_title: "Voer titel in",
+ enter_title_hint: "Voer een geldige titel in voor het nieuwe vak",
+ assign_self: "Wijs mezelf toe als lesgever",
+ field_required: "Dit veld is verplicht",
+ field_length: "Titel moet minstens 3 tekens lang zijn",
+ subject_instructors: "Vak verantwoordelijken",
+ search_for_instructors: "Zoek naar vak verantwoordelijken",
+ email: "Vak email",
+ enter_email: "Voer email in",
+ email_hint: "Veld is optioneel",
+ email_invalid: "Ingevoerde email is ongeldig",
+ },
+
group: {
not_found: "Groep niet gevonden",
not_found2: "Geen groepen teruggevonden",
diff --git a/frontend/src/models/Subject.ts b/frontend/src/models/Subject.ts
index 9a391ec4..e965ed4c 100644
--- a/frontend/src/models/Subject.ts
+++ b/frontend/src/models/Subject.ts
@@ -26,3 +26,9 @@ export interface SubjectDetails {
subjectData: Subject;
role: SubjectRole;
}
+
+export default interface SubjectForm {
+ name: string;
+ email: string;
+ academic_year: number;
+}
diff --git a/frontend/src/queries/Subject.ts b/frontend/src/queries/Subject.ts
index 261a2cca..dad0cd48 100644
--- a/frontend/src/queries/Subject.ts
+++ b/frontend/src/queries/Subject.ts
@@ -10,12 +10,15 @@ import {
getSubjectByUuid,
registerToSubject,
getSubjectUuid,
+ createSubject,
+ createSubjectInstructor,
} from "@/services/subject";
import { getSubjectProjects } from "@/services/project";
import type User from "@/models/User";
import type Subject from "@/models/Subject";
import type { UserSubjectList } from "@/models/Subject";
import type Project from "@/models/Project";
+import type SubjectForm from "@/models/Subject";
function SUBJECT_QUERY_KEY(subjectId: number | string): (string | number)[] {
return ["subject", subjectId];
@@ -147,9 +150,67 @@ export function useRegisterToSubjectMutation(): UseMutationReturnType<
onSettled: () => {
queryClient.invalidateQueries({ queryKey: SUBJECTS_QUERY_KEY() });
},
- onError: (error) => {
- console.error(error);
+ onError: () => {
alert("Failed to register to subject");
},
});
}
+
+/**
+ * Mutation composable for creating a subject
+ */
+export function useCreateSubjectMutation(): UseMutationReturnType<
+ number,
+ Error,
+ SubjectForm,
+ void
+> {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: createSubject,
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: SUBJECTS_QUERY_KEY() });
+ },
+ onError: () => {
+ alert("Could not create subject. Please try again.");
+ },
+ });
+}
+
+/**
+ * Mutation composable for creating subject instructor
+ */
+
+export function useCreateSubjectInstructorMutation(): UseMutationReturnType<
+ void,
+ Error,
+ { user: User; subjectId: number },
+ { previousUsers: User[] }
+> {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: ({ user, subjectId }) => createSubjectInstructor(subjectId, user.uid),
+ onMutate: ({ subjectId, user }) => {
+ const previousUsers = queryClient.getQueryData(
+ SUBJECT_INSTRUCTORS_QUERY_KEY(subjectId)
+ );
+ queryClient.cancelQueries({ queryKey: SUBJECT_INSTRUCTORS_QUERY_KEY(subjectId) });
+ const newUsers = previousUsers ? [...previousUsers] : [];
+ newUsers.push(user);
+ queryClient.setQueryData(SUBJECT_INSTRUCTORS_QUERY_KEY(subjectId), newUsers);
+ return { previousUsers: previousUsers || [] };
+ },
+ onSuccess: (_, { subjectId }) => {
+ queryClient.invalidateQueries({
+ queryKey: SUBJECT_INSTRUCTORS_QUERY_KEY(subjectId),
+ });
+ },
+ onError: (_, { subjectId }, ctx) => {
+ queryClient.setQueryData(
+ SUBJECT_INSTRUCTORS_QUERY_KEY(subjectId),
+ () => ctx!.previousUsers!
+ );
+ alert("Could not create subject instructor. Please try again.");
+ },
+ });
+}
diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts
index f332d648..5cfedfde 100644
--- a/frontend/src/router/index.ts
+++ b/frontend/src/router/index.ts
@@ -119,6 +119,11 @@ const router = createRouter({
name: "subjects",
component: () => import("../views/subject/SubjectsView.vue"),
},
+ {
+ path: "/subjects/create",
+ name: "create-subject",
+ component: () => import("../views/subject/CreateSubjectView.vue"),
+ },
{
path: "/subjects/:subjectId(\\d+)",
name: "subject",
diff --git a/frontend/src/services/subject.ts b/frontend/src/services/subject.ts
index 328c8e79..601b5864 100644
--- a/frontend/src/services/subject.ts
+++ b/frontend/src/services/subject.ts
@@ -2,6 +2,7 @@ import type User from "@/models/User";
import type Subject from "@/models/Subject";
import type { UserSubjectList } from "@/models/Subject";
import { authorized_fetch } from "@/services";
+import type SubjectForm from "@/models/Subject";
/**
* Fetches the subject with the given ID.
@@ -53,3 +54,20 @@ export async function getSubjectUuid(subjectId: number): Promise {
const result = await authorized_fetch(`/api/subjects/${subjectId}/uuid`, { method: "GET" });
return result.subject_uuid;
}
+
+/**
+ * Creates a new project.
+ */
+export async function createSubject(projectData: SubjectForm): Promise {
+ const response = await authorized_fetch(`/api/subjects/`, {
+ method: "POST",
+ body: JSON.stringify(projectData),
+ });
+ return response.id;
+}
+
+export async function createSubjectInstructor(subjectId: number, uid: string): Promise {
+ return authorized_fetch(`/api/subjects/${subjectId}/instructors?user_id=${uid}`, {
+ method: "POST",
+ });
+}
diff --git a/frontend/src/utils.ts b/frontend/src/utils.ts
index db352b29..0efa3729 100644
--- a/frontend/src/utils.ts
+++ b/frontend/src/utils.ts
@@ -12,3 +12,33 @@ export async function download_file(url: string, filename: string) {
document.body.appendChild(link);
link.click();
}
+
+type ThrottledFunction any> = (...args: Parameters) => void;
+
+export function throttle any>(
+ func: T,
+ limit: number
+): ThrottledFunction {
+ let lastFunc: ReturnType;
+ let lastRan: number;
+
+ return function (this: any, ...args: Parameters) {
+ const context = this;
+
+ if (!lastRan) {
+ func.apply(context, args);
+ lastRan = Date.now();
+ } else {
+ clearTimeout(lastFunc);
+ lastFunc = setTimeout(
+ function () {
+ if (Date.now() - lastRan >= limit) {
+ func.apply(context, args);
+ lastRan = Date.now();
+ }
+ },
+ limit - (Date.now() - lastRan)
+ );
+ }
+ };
+}
diff --git a/frontend/src/views/AdminView.vue b/frontend/src/views/AdminView.vue
index 59295e3a..53a746f1 100644
--- a/frontend/src/views/AdminView.vue
+++ b/frontend/src/views/AdminView.vue
@@ -3,129 +3,40 @@
-
-
-
-
-
- onToggleTeacher(item)"
- >
-
-
- onToggleAdmin(item)"
- >
-
-
- onDeleteUser(item)">
- mdi-delete
-
-
-
+
diff --git a/frontend/src/views/subject/SubjectView.vue b/frontend/src/views/subject/SubjectView.vue
index 2e05df13..fb7cffeb 100644
--- a/frontend/src/views/subject/SubjectView.vue
+++ b/frontend/src/views/subject/SubjectView.vue
@@ -28,7 +28,7 @@
{
const isStudent = computed(() => {
return [...(students.value || [])].some((student) => student?.uid === user.value?.uid);
});
+
+const sortedInstructors = computed(() => {
+ return [...(instructors.value || [])].sort((a, b) => {
+ if (a?.is_teacher && !b?.is_teacher) {
+ return -1;
+ } else if (!a?.is_teacher && b?.is_teacher) {
+ return 1;
+ } else {
+ return a?.surname.localeCompare(b?.surname);
+ }
+ });
+});
+
const { isAdmin } = useIsAdmin();
const { isTeacher } = useIsTeacher();
diff --git a/frontend/src/views/subject/SubjectsView.vue b/frontend/src/views/subject/SubjectsView.vue
index a5aec499..e15e46ee 100644
--- a/frontend/src/views/subject/SubjectsView.vue
+++ b/frontend/src/views/subject/SubjectsView.vue
@@ -38,7 +38,7 @@
-
+
{{ $t("subjects.create_subject") }}
@@ -52,8 +52,8 @@