diff --git a/backend/src/controllers/students.ts b/backend/src/controllers/students.ts index 229cff7e..e4c49683 100644 --- a/backend/src/controllers/students.ts +++ b/backend/src/controllers/students.ts @@ -113,7 +113,7 @@ export async function createStudentRequestHandler(req: Request, res: Response): const classId = req.body.classId; requireFields({ username, classId }); - const request = await createClassJoinRequest(username, classId); + const request = await createClassJoinRequest(username, classId.toUpperCase()); res.json({ request }); } diff --git a/frontend/src/components/LearningPathSearchField.vue b/frontend/src/components/LearningPathSearchField.vue index b8b71960..9afd62f6 100644 --- a/frontend/src/components/LearningPathSearchField.vue +++ b/frontend/src/components/LearningPathSearchField.vue @@ -31,4 +31,9 @@ > - + diff --git a/frontend/src/components/LearningPathsGrid.vue b/frontend/src/components/LearningPathsGrid.vue index 865c7166..8df08a00 100644 --- a/frontend/src/components/LearningPathsGrid.vue +++ b/frontend/src/components/LearningPathsGrid.vue @@ -53,9 +53,9 @@ white-space: normal; } .results-grid { - margin: 20px; + margin: 20px auto; display: flex; - align-items: stretch; + justify-content: center; gap: 20px; flex-wrap: wrap; } diff --git a/frontend/src/components/MenuBar.vue b/frontend/src/components/MenuBar.vue index a58be2f8..a4652236 100644 --- a/frontend/src/components/MenuBar.vue +++ b/frontend/src/components/MenuBar.vue @@ -14,6 +14,7 @@ const _router = useRouter(); // Zonder '_' gaf dit een linter error voor unused variable const name: string = auth.authState.user!.profile.name!; + const username = auth.authState.user!.profile.preferred_username!; const email = auth.authState.user!.profile.email; const initials: string = name .split(" ") @@ -180,10 +181,15 @@ - - {{ initials }} + + {{ initials }} {{ name }} + {{ username }} {{ email }} { // Verify if user is logged in before accessing certain routes if (to.meta.requiresAuth) { if (!authService.isLoggedIn.value && !(await authService.loadUser())) { - next("/login"); + const path = to.fullPath; + if (allowRedirect(path)) { + localStorage.setItem(Redirect.AFTER_LOGIN_KEY, path); + } + next(Redirect.LOGIN); } else { next(); } diff --git a/frontend/src/utils/redirect.ts b/frontend/src/utils/redirect.ts new file mode 100644 index 00000000..f3ec0e75 --- /dev/null +++ b/frontend/src/utils/redirect.ts @@ -0,0 +1,12 @@ +export enum Redirect { + AFTER_LOGIN_KEY = "redirectAfterLogin", + HOME = "/user", + LOGIN = "/login", + ROOT = "/", +} + +const NOT_ALLOWED_REDIRECTS = new Set([Redirect.HOME, Redirect.ROOT, Redirect.LOGIN]); + +export function allowRedirect(path: string): boolean { + return !NOT_ALLOWED_REDIRECTS.has(path as Redirect); +} diff --git a/frontend/src/views/CallbackPage.vue b/frontend/src/views/CallbackPage.vue index cd004eae..d4d300e4 100644 --- a/frontend/src/views/CallbackPage.vue +++ b/frontend/src/views/CallbackPage.vue @@ -3,6 +3,7 @@ import { useI18n } from "vue-i18n"; import { onMounted, ref, type Ref } from "vue"; import auth from "../services/auth/auth-service.ts"; + import { Redirect } from "@/utils/redirect.ts"; const { t } = useI18n(); @@ -10,10 +11,20 @@ const errorMessage: Ref = ref(null); + async function redirectPage(): Promise { + const redirectUrl = localStorage.getItem(Redirect.AFTER_LOGIN_KEY); + if (redirectUrl) { + localStorage.removeItem(Redirect.AFTER_LOGIN_KEY); + await router.replace(redirectUrl); + } else { + await router.replace(Redirect.HOME); + } + } + onMounted(async () => { try { await auth.handleLoginCallback(); - await router.replace("/user"); // Redirect to theme page + await redirectPage(); } catch (error) { errorMessage.value = `${t("loginUnexpectedError")}: ${error}`; } diff --git a/frontend/src/views/SingleTheme.vue b/frontend/src/views/SingleTheme.vue index c858ac26..1cd9afab 100644 --- a/frontend/src/views/SingleTheme.vue +++ b/frontend/src/views/SingleTheme.vue @@ -35,13 +35,14 @@ - + {{ currentThemeInfo!!.title }} {{ currentThemeInfo!!.description }} - + + .search-field-container { - display: block; - margin: 20px; + justify-content: center !important; } .search-field { - max-width: 300px; + width: 25%; + min-width: 300px; } .container { padding: 20px; + justify-content: center; + justify-items: center; } diff --git a/frontend/src/views/classes/StudentClasses.vue b/frontend/src/views/classes/StudentClasses.vue index 27cdde73..391930b3 100644 --- a/frontend/src/views/classes/StudentClasses.vue +++ b/frontend/src/views/classes/StudentClasses.vue @@ -2,7 +2,7 @@ import { useI18n } from "vue-i18n"; import authState from "@/services/auth/auth-service.ts"; import { computed, onMounted, ref } from "vue"; - import { validate, version } from "uuid"; + import { useRoute } from "vue-router"; import type { ClassDTO } from "@dwengo-1/common/interfaces/class"; import { useCreateJoinRequestMutation, useStudentClassesQuery } from "@/queries/students"; import type { StudentDTO } from "@dwengo-1/common/interfaces/student"; @@ -15,6 +15,7 @@ import "../../assets/common.css"; const { t } = useI18n(); + const route = useRoute(); // Username of logged in student const username = ref(undefined); @@ -38,6 +39,11 @@ } finally { isLoading.value = false; } + + const queryCode = route.query.code as string | undefined; + if (queryCode) { + code.value = queryCode; + } }); // Fetch all classes of the logged in student @@ -75,11 +81,15 @@ // The code a student sends in to join a class needs to be formatted as v4 to be valid // These rules are used to display a message to the user if they use a code that has an invalid format + function codeRegex(value: string): boolean { + return /^[a-zA-Z0-9]{6}$/.test(value); + } + const codeRules = [ (value: string | undefined): string | boolean => { if (value === undefined || value === "") { return true; - } else if (value !== undefined && validate(value) && version(value) === 4) { + } else if (codeRegex(value)) { return true; } return t("invalidFormat"); @@ -92,7 +102,7 @@ // Function called when a student submits a code to join a class function submitCode(): void { // Check if the code is valid - if (code.value !== undefined && validate(code.value) && version(code.value) === 4) { + if (code.value !== undefined && codeRegex(code.value)) { mutate( { username: username.value!, classId: code.value }, { @@ -260,7 +270,7 @@ diff --git a/frontend/src/views/classes/TeacherClasses.vue b/frontend/src/views/classes/TeacherClasses.vue index c818a046..cb2dbcac 100644 --- a/frontend/src/views/classes/TeacherClasses.vue +++ b/frontend/src/views/classes/TeacherClasses.vue @@ -132,17 +132,12 @@ // Show the teacher, copying of the code was a successs const copied = ref(false); - // Copy the generated code to the clipboard - async function copyToClipboard(): Promise { - await navigator.clipboard.writeText(code.value); - copied.value = true; - } + async function copyToClipboard(code: string, isDialog = false, isLink = false): Promise { + const content = isLink ? `${window.location.origin}/user/class?code=${code}` : code; + await navigator.clipboard.writeText(content); + copied.value = isDialog; - async function copyCode(selectedCode: string): Promise { - code.value = selectedCode; - await copyToClipboard(); - showSnackbar(t("copied"), "white"); - copied.value = false; + if (!isDialog) showSnackbar(t("copied"), "white"); } // Custom breakpoints @@ -236,20 +231,34 @@ - - {{ c.id }} - + + {{ c.id }} + + + mdi-link-variant + + + > + + {{ c.students.length }} @@ -311,14 +320,29 @@ max-width="400px" > - code + {{ t("code") }} + > + + + mdi-content-copy + + + mdi-link-variant + + + + > + + + mdi-content-copy + + + mdi-link-variant + + + - - + + + + + - - - - + + - - - - - - - - - - - - + + + +
{{ username }}
{{ email }}
{{ currentThemeInfo!!.description }}