Merge branch 'dev' into github-actions/coverage

This commit is contained in:
Timo De Meyst 2025-04-14 11:00:56 +02:00
commit bd266d3594
111 changed files with 3698 additions and 2908 deletions

View file

@ -21,6 +21,7 @@
"@tanstack/vue-query": "^5.69.0",
"axios": "^1.8.2",
"oidc-client-ts": "^3.1.0",
"uuid": "^11.1.0",
"vue": "^3.5.13",
"vue-i18n": "^11.1.2",
"vue-router": "^4.5.0",

View file

@ -0,0 +1,39 @@
import type { AnswerData, AnswerDTO, AnswerId } from "@dwengo-1/common/interfaces/answer";
import { BaseController } from "@/controllers/base-controller.ts";
import type { QuestionId } from "@dwengo-1/common/interfaces/question";
export interface AnswersResponse {
answers: AnswerDTO[] | AnswerId[];
}
export interface AnswerResponse {
answer: AnswerDTO;
}
export class AnswerController extends BaseController {
constructor(questionId: QuestionId) {
this.loId = questionId.learningObjectIdentifier;
this.sequenceNumber = questionId.sequenceNumber;
super(`learningObject/${loId.hruid}/:${loId.version}/questions/${this.sequenceNumber}/answers`);
}
async getAll(full = true): Promise<AnswersResponse> {
return this.get<AnswersResponse>("/", { lang: this.loId.lang, full });
}
async getBy(seq: number): Promise<AnswerResponse> {
return this.get<AnswerResponse>(`/${seq}`, { lang: this.loId.lang });
}
async create(answerData: AnswerData): Promise<AnswerResponse> {
return this.post<AnswerResponse>("/", answerData, { lang: this.loId.lang });
}
async remove(seq: number): Promise<AnswerResponse> {
return this.delete<AnswerResponse>(`/${seq}`, { lang: this.loId.lang });
}
async update(seq: number, answerData: AnswerData): Promise<AnswerResponse> {
return this.put<AnswerResponse>(`/${seq}`, answerData, { lang: this.loId.lang });
}
}

View file

@ -1,5 +1,51 @@
import type { AssignmentDTO } from "@dwengo-1/interfaces/assignment";
import { BaseController } from "./base-controller";
import type { AssignmentDTO } from "@dwengo-1/common/interfaces/assignment";
import type { SubmissionsResponse } from "./submissions";
import type { QuestionsResponse } from "./questions";
import type { GroupsResponse } from "./groups";
export interface AssignmentsResponse {
assignments: AssignmentDTO[];
} // TODO ID
assignments: AssignmentDTO[] | string[];
}
export interface AssignmentResponse {
assignment: AssignmentDTO;
}
export class AssignmentController extends BaseController {
constructor(classid: string) {
super(`class/${classid}/assignments`);
}
async getAll(full = true): Promise<AssignmentsResponse> {
return this.get<AssignmentsResponse>(`/`, { full });
}
async getByNumber(num: number): Promise<AssignmentResponse> {
return this.get<AssignmentResponse>(`/${num}`);
}
async createAssignment(data: AssignmentDTO): Promise<AssignmentResponse> {
return this.post<AssignmentResponse>(`/`, data);
}
async deleteAssignment(num: number): Promise<AssignmentResponse> {
return this.delete<AssignmentResponse>(`/${num}`);
}
async updateAssignment(num: number, data: Partial<AssignmentDTO>): Promise<AssignmentResponse> {
return this.put<AssignmentResponse>(`/${num}`, data);
}
async getSubmissions(assignmentNumber: number, full = true): Promise<SubmissionsResponse> {
return this.get<SubmissionsResponse>(`/${assignmentNumber}/submissions`, { full });
}
async getQuestions(assignmentNumber: number, full = true): Promise<QuestionsResponse> {
return this.get<QuestionsResponse>(`/${assignmentNumber}/questions`, { full });
}
async getGroups(assignmentNumber: number, full = true): Promise<GroupsResponse> {
return this.get<GroupsResponse>(`/${assignmentNumber}/groups`, { full });
}
}

View file

@ -10,7 +10,7 @@ export abstract class BaseController {
}
private static assertSuccessResponse(response: AxiosResponse<unknown, unknown>): void {
if (response.status / 100 !== 2) {
if (response.status < 200 || response.status >= 300) {
throw new HttpErrorResponseException(response);
}
}
@ -21,20 +21,20 @@ export abstract class BaseController {
return response.data;
}
protected async post<T>(path: string, body: unknown): Promise<T> {
const response = await apiClient.post<T>(this.absolutePathFor(path), body);
protected async post<T>(path: string, body: unknown, queryParams?: QueryParams): Promise<T> {
const response = await apiClient.post<T>(this.absolutePathFor(path), body, { params: queryParams });
BaseController.assertSuccessResponse(response);
return response.data;
}
protected async delete<T>(path: string): Promise<T> {
const response = await apiClient.delete<T>(this.absolutePathFor(path));
protected async delete<T>(path: string, queryParams?: QueryParams): Promise<T> {
const response = await apiClient.delete<T>(this.absolutePathFor(path), { params: queryParams });
BaseController.assertSuccessResponse(response);
return response.data;
}
protected async put<T>(path: string, body: unknown): Promise<T> {
const response = await apiClient.put<T>(this.absolutePathFor(path), body);
protected async put<T>(path: string, body: unknown, queryParams?: QueryParams): Promise<T> {
const response = await apiClient.put<T>(this.absolutePathFor(path), body, { params: queryParams });
BaseController.assertSuccessResponse(response);
return response.data;
}

View file

@ -1,5 +1,80 @@
import type { ClassDTO } from "@dwengo-1/interfaces/class";
import { BaseController } from "./base-controller";
import type { ClassDTO } from "@dwengo-1/common/interfaces/class";
import type { StudentsResponse } from "./students";
import type { AssignmentsResponse } from "./assignments";
import type { TeacherInvitationDTO } from "@dwengo-1/common/interfaces/teacher-invitation";
import type { TeachersResponse } from "@/controllers/teachers.ts";
export interface ClassesResponse {
classes: ClassDTO[] | string[];
}
export interface ClassResponse {
class: ClassDTO;
}
export interface TeacherInvitationsResponse {
invites: TeacherInvitationDTO[];
}
export interface TeacherInvitationResponse {
invite: TeacherInvitationDTO;
}
export class ClassController extends BaseController {
constructor() {
super("class");
}
async getAll(full = true): Promise<ClassesResponse> {
return this.get<ClassesResponse>(`/`, { full });
}
async getById(id: string): Promise<ClassResponse> {
return this.get<ClassResponse>(`/${id}`);
}
async createClass(data: ClassDTO): Promise<ClassResponse> {
return this.post<ClassResponse>(`/`, data);
}
async deleteClass(id: string): Promise<ClassResponse> {
return this.delete<ClassResponse>(`/${id}`);
}
async updateClass(id: string, data: Partial<ClassDTO>): Promise<ClassResponse> {
return this.put<ClassResponse>(`/${id}`, data);
}
async getStudents(id: string, full = true): Promise<StudentsResponse> {
return this.get<StudentsResponse>(`/${id}/students`, { full });
}
async addStudent(id: string, username: string): Promise<ClassResponse> {
return this.post<ClassResponse>(`/${id}/students`, { username });
}
async deleteStudent(id: string, username: string): Promise<ClassResponse> {
return this.delete<ClassResponse>(`/${id}/students/${username}`);
}
async getTeachers(id: string, full = true): Promise<TeachersResponse> {
return this.get<TeachersResponse>(`/${id}/teachers`, { full });
}
async addTeacher(id: string, username: string): Promise<ClassResponse> {
return this.post<ClassResponse>(`/${id}/teachers`, { username });
}
async deleteTeacher(id: string, username: string): Promise<ClassResponse> {
return this.delete<ClassResponse>(`/${id}/teachers/${username}`);
}
async getTeacherInvitations(id: string, full = true): Promise<TeacherInvitationsResponse> {
return this.get<TeacherInvitationsResponse>(`/${id}/teacher-invitations`, { full });
}
async getAssignments(id: string, full = true): Promise<AssignmentsResponse> {
return this.get<AssignmentsResponse>(`/${id}/assignments`, { full });
}
}

View file

@ -1,6 +1,7 @@
import { ThemeController } from "@/controllers/themes.ts";
import { LearningObjectController } from "@/controllers/learning-objects.ts";
import { LearningPathController } from "@/controllers/learning-paths.ts";
import { ClassController } from "@/controllers/classes.ts";
export function controllerGetter<T>(factory: new () => T): () => T {
let instance: T | undefined;
@ -16,3 +17,4 @@ export function controllerGetter<T>(factory: new () => T): () => T {
export const getThemeController = controllerGetter(ThemeController);
export const getLearningObjectController = controllerGetter(LearningObjectController);
export const getLearningPathController = controllerGetter(LearningPathController);
export const getClassController = controllerGetter(ClassController);

View file

@ -1,5 +1,46 @@
import type { GroupDTO } from "@dwengo-1/interfaces/group";
import { BaseController } from "./base-controller";
import type { GroupDTO } from "@dwengo-1/common/interfaces/group";
import type { SubmissionsResponse } from "./submissions";
import type { QuestionsResponse } from "./questions";
export interface GroupsResponse {
groups: GroupDTO[];
} // | TODO id
}
export interface GroupResponse {
group: GroupDTO;
}
export class GroupController extends BaseController {
constructor(classid: string, assignmentNumber: number) {
super(`class/${classid}/assignments/${assignmentNumber}/groups`);
}
async getAll(full = true): Promise<GroupsResponse> {
return this.get<GroupsResponse>(`/`, { full });
}
async getByNumber(num: number): Promise<GroupResponse> {
return this.get<GroupResponse>(`/${num}`);
}
async createGroup(data: GroupDTO): Promise<GroupResponse> {
return this.post<GroupResponse>(`/`, data);
}
async deleteGroup(num: number): Promise<GroupResponse> {
return this.delete<GroupResponse>(`/${num}`);
}
async updateGroup(num: number, data: Partial<GroupDTO>): Promise<GroupResponse> {
return this.put<GroupResponse>(`/${num}`, data);
}
async getSubmissions(groupNumber: number, full = true): Promise<SubmissionsResponse> {
return this.get<SubmissionsResponse>(`/${groupNumber}/submissions`, { full });
}
async getQuestions(groupNumber: number, full = true): Promise<QuestionsResponse> {
return this.get<QuestionsResponse>(`/${groupNumber}/questions`, { full });
}
}

View file

@ -1,5 +1,38 @@
import type { QuestionDTO, QuestionId } from "@dwengo-1/interfaces/question";
import type { QuestionData, QuestionDTO, QuestionId } from "@dwengo-1/common/interfaces/question";
import { BaseController } from "@/controllers/base-controller.ts";
import type { LearningObjectIdentifierDTO } from "@dwengo-1/common/interfaces/learning-content";
export interface QuestionsResponse {
questions: QuestionDTO[] | QuestionId[];
}
export interface QuestionResponse {
question: QuestionDTO;
}
export class QuestionController extends BaseController {
constructor(loId: LearningObjectIdentifierDTO) {
this.loId = loId;
super(`learningObject/${loId.hruid}/:${loId.version}/questions`);
}
async getAll(full = true): Promise<QuestionsResponse> {
return this.get<QuestionsResponse>("/", { lang: this.loId.lang, full });
}
async getBy(sequenceNumber: number): Promise<QuestionResponse> {
return this.get<QuestionResponse>(`/${sequenceNumber}`, { lang: this.loId.lang });
}
async create(questionData: QuestionData): Promise<QuestionResponse> {
return this.post<QuestionResponse>("/", questionData, { lang: this.loId.lang });
}
async remove(sequenceNumber: number): Promise<QuestionResponse> {
return this.delete<QuestionResponse>(`/${sequenceNumber}`, { lang: this.loId.lang });
}
async update(sequenceNumber: number, questionData: QuestionData): Promise<QuestionResponse> {
return this.put<QuestionResponse>(`/${sequenceNumber}`, questionData, { lang: this.loId.lang });
}
}

View file

@ -4,8 +4,8 @@ import type { AssignmentsResponse } from "@/controllers/assignments.ts";
import type { GroupsResponse } from "@/controllers/groups.ts";
import type { SubmissionsResponse } from "@/controllers/submissions.ts";
import type { QuestionsResponse } from "@/controllers/questions.ts";
import type { StudentDTO } from "@dwengo-1/interfaces/student";
import type { ClassJoinRequestDTO } from "@dwengo-1/interfaces/class-join-request";
import type { StudentDTO } from "@dwengo-1/common/interfaces/student";
import type { ClassJoinRequestDTO } from "@dwengo-1/common/interfaces/class-join-request";
export interface StudentsResponse {
students: StudentDTO[] | string[];
@ -70,7 +70,7 @@ export class StudentController extends BaseController {
}
async createJoinRequest(username: string, classId: string): Promise<JoinRequestResponse> {
return this.post<JoinRequestResponse>(`/${username}/joinRequests}`, classId);
return this.post<JoinRequestResponse>(`/${username}/joinRequests`, { classId });
}
async deleteJoinRequest(username: string, classId: string): Promise<JoinRequestResponse> {

View file

@ -1,5 +1,32 @@
import { type SubmissionDTO, SubmissionDTOId } from "@dwengo-1/interfaces/submission";
import { BaseController } from "./base-controller";
import type { SubmissionDTO, SubmissionDTOId } from "@dwengo-1/common/interfaces/submission";
export interface SubmissionsResponse {
submissions: SubmissionDTO[] | SubmissionDTOId[];
}
export interface SubmissionResponse {
submission: SubmissionDTO;
}
export class SubmissionController extends BaseController {
constructor(classid: string, assignmentNumber: number, groupNumber: number) {
super(`class/${classid}/assignments/${assignmentNumber}/groups/${groupNumber}`);
}
async getAll(full = true): Promise<SubmissionsResponse> {
return this.get<SubmissionsResponse>(`/`, { full });
}
async getByNumber(submissionNumber: number): Promise<SubmissionResponse> {
return this.get<SubmissionResponse>(`/${submissionNumber}`);
}
async createSubmission(data: unknown): Promise<SubmissionResponse> {
return this.post<SubmissionResponse>(`/`, data);
}
async deleteSubmission(submissionNumber: number): Promise<SubmissionResponse> {
return this.delete<SubmissionResponse>(`/${submissionNumber}`);
}
}

View file

@ -2,7 +2,7 @@ import { BaseController } from "@/controllers/base-controller.ts";
import type { JoinRequestResponse, JoinRequestsResponse, StudentsResponse } from "@/controllers/students.ts";
import type { QuestionsResponse } from "@/controllers/questions.ts";
import type { ClassesResponse } from "@/controllers/classes.ts";
import type { TeacherDTO } from "@dwengo-1/interfaces/teacher";
import type { TeacherDTO } from "@dwengo-1/common/interfaces/teacher";
export interface TeachersResponse {
teachers: TeacherDTO[] | string[];

View file

@ -1,5 +1,5 @@
import { BaseController } from "@/controllers/base-controller.ts";
import type { Theme } from "@dwengo-1/interfaces/theme";
import type { Theme } from "@dwengo-1/common/interfaces/theme";
export class ThemeController extends BaseController {
constructor() {

View file

@ -17,6 +17,11 @@
"inclusive": "Inclusiv",
"sociallyRelevant": "Gesellschaftlich relevant",
"translate": "übersetzen",
"joinClass": "Klasse beitreten",
"JoinClassExplanation": "Geben Sie den Code ein, den Ihnen die Lehrkraft mitgeteilt hat, um der Klasse beizutreten.",
"invalidFormat": "Ungültiges Format",
"submitCode": "senden",
"members": "Mitglieder",
"themes": "Themen",
"choose-theme": "Wähle ein thema",
"choose-age": "Alter auswählen",
@ -51,5 +56,24 @@
"noLearningPathsFoundDescription": "Es gibt keine Lernpfade, die zu Ihrem Suchbegriff passen.",
"legendNotCompletedYet": "Noch nicht fertig",
"legendCompleted": "Fertig",
"legendTeacherExclusive": "Information für Lehrkräfte"
"legendTeacherExclusive": "Information für Lehrkräfte",
"code": "code",
"class": "Klasse",
"invitations": "Einladungen",
"createClass": "Klasse erstellen",
"createClassInstructions": "Geben Sie einen Namen für Ihre Klasse ein und klicken Sie auf „Erstellen“. Es erscheint ein Fenster mit einem Code, den Sie kopieren können. Geben Sie diesen Code an Ihre Schüler weiter und sie können Ihrer Klasse beitreten.",
"classname": "Klassenname",
"EnterNameOfClass": "einen Klassennamen eingeben.",
"create": "erstellen",
"sender": "Absender",
"nameIsMandatory": "Der Klassenname ist ein Pflichtfeld",
"onlyUse": "nur Buchstaben, Zahlen, Bindestriche (-) und Unterstriche (_) verwenden",
"close": "schließen",
"copied": "kopiert!",
"accept": "akzeptieren",
"deny": "ablehnen",
"sent": "sent",
"failed": "gescheitert",
"wrong": "etwas ist schief gelaufen",
"created": "erstellt"
}

View file

@ -29,6 +29,11 @@
"sociallyRelevant": "Socially relevant",
"login": "log in",
"translate": "translate",
"joinClass": "Join class",
"JoinClassExplanation": "Enter the code the teacher has given you to join the class.",
"invalidFormat": "Invalid format.",
"submitCode": "submit",
"members": "members",
"themes": "Themes",
"choose-theme": "Select a theme",
"choose-age": "Select age",
@ -51,5 +56,24 @@
"high-school": "16-18 years old",
"older": "18 and older"
},
"read-more": "Read more"
"read-more": "Read more",
"code": "code",
"class": "class",
"invitations": "invitations",
"createClass": "create class",
"classname": "classname",
"EnterNameOfClass": "Enter a classname.",
"create": "create",
"sender": "sender",
"nameIsMandatory": "classname is mandatory",
"onlyUse": "only use letters, numbers, dashes (-) and underscores (_)",
"close": "close",
"copied": "copied!",
"accept": "accept",
"deny": "deny",
"createClassInstructions": "Enter a name for your class and click on create. A window will appear with a code that you can copy. Give this code to your students and they will be able to join.",
"sent": "sent",
"failed": "failed",
"wrong": "something went wrong",
"created": "created"
}

View file

@ -29,6 +29,11 @@
"inclusive": "Inclusif",
"sociallyRelevant": "Socialement pertinent",
"translate": "traduire",
"joinClass": "Rejoindre une classe",
"JoinClassExplanation": "Entrez le code que l'enseignant vous a donné pour rejoindre la classe.",
"invalidFormat": "Format non valide.",
"submitCode": "envoyer",
"members": "membres",
"themes": "Thèmes",
"choose-theme": "Choisis un thème",
"choose-age": "Choisis un âge",
@ -51,5 +56,24 @@
"high-school": "16-18 ans",
"older": "18 et plus"
},
"read-more": "En savoir plus"
"read-more": "En savoir plus",
"code": "code",
"class": "classe",
"invitations": "invitations",
"createClass": "créer une classe",
"createClassInstructions": "Entrez un nom pour votre classe et cliquez sur créer. Une fenêtre apparaît avec un code que vous pouvez copier. Donnez ce code à vos élèves et ils pourront rejoindre votre classe.",
"classname": "nom de classe",
"EnterNameOfClass": "saisir un nom de classe.",
"create": "créer",
"sender": "expéditeur",
"nameIsMandatory": "le nom de classe est obligatoire",
"onlyUse": "n'utiliser que des lettres, des chiffres, des tirets (-) et des traits de soulignement (_)",
"close": "fermer",
"copied": "copié!",
"accept": "accepter",
"deny": "refuser",
"sent": "envoyé",
"failed": "échoué",
"wrong": "quelque chose n'a pas fonctionné",
"created": "créé"
}

View file

@ -29,6 +29,11 @@
"sociallyRelevant": "Maatschappelijk relevant",
"login": "log in",
"translate": "vertalen",
"joinClass": "Word lid van een klas",
"JoinClassExplanation": "Voer de code in die je van de docent hebt gekregen om lid te worden van de klas.",
"invalidFormat": "Ongeldig formaat.",
"submitCode": "verzenden",
"members": "leden",
"themes": "Lesthema's",
"choose-theme": "Kies een thema",
"choose-age": "Kies een leeftijd",
@ -51,5 +56,24 @@
"high-school": "3e graad secundair",
"older": "Hoger onderwijs"
},
"read-more": "Lees meer"
"read-more": "Lees meer",
"code": "code",
"class": "klas",
"invitations": "uitnodigingen",
"createClass": "klas aanmaken",
"createClassInstructions": "Voer een naam in voor je klas en klik op create. Er verschijnt een venster met een code die je kunt kopiëren. Geef deze code aan je leerlingen en ze kunnen deelnemen aan je klas.",
"classname": "klasnaam",
"EnterNameOfClass": "Geef een klasnaam op.",
"create": "aanmaken",
"sender": "afzender",
"nameIsMandatory": "klasnaam is verplicht",
"onlyUse": "gebruik enkel letters, cijfers, dashes (-) en underscores (_)",
"close": "sluiten",
"copied": "gekopieerd!",
"accept": "accepteren",
"deny": "weigeren",
"sent": "verzonden",
"failed": "mislukt",
"wrong": "er ging iets verkeerd",
"created": "gecreëerd"
}

View file

@ -0,0 +1,51 @@
import type { QuestionId } from "@dwengo-1/common/dist/interfaces/question.ts";
import { type MaybeRefOrGetter, toValue } from "vue";
import { useMutation, type UseMutationReturnType, useQuery, type UseQueryReturnType } from "@tanstack/vue-query";
import { AnswerController, type AnswerResponse, type AnswersResponse } from "@/controllers/answers.ts";
import type { AnswerData } from "@dwengo-1/common/dist/interfaces/answer.ts";
// TODO caching
export function useAnswersQuery(
questionId: MaybeRefOrGetter<QuestionId>,
full: MaybeRefOrGetter<boolean> = true,
): UseQueryReturnType<AnswersResponse, Error> {
return useQuery({
queryFn: async () => new AnswerController(toValue(questionId)).getAll(toValue(full)),
enabled: () => Boolean(toValue(questionId)),
});
}
export function useAnswerQuery(
questionId: MaybeRefOrGetter<QuestionId>,
sequenceNumber: MaybeRefOrGetter<number>,
): UseQueryReturnType<AnswerResponse, Error> {
return useQuery({
queryFn: async () => new AnswerController(toValue(questionId)).getBy(toValue(sequenceNumber)),
enabled: () => Boolean(toValue(questionId)),
});
}
export function useCreateAnswerMutation(
questionId: MaybeRefOrGetter<QuestionId>,
): UseMutationReturnType<AnswerResponse, Error, AnswerData, unknown> {
return useMutation({
mutationFn: async (data) => new AnswerController(toValue(questionId)).create(data),
});
}
export function useDeleteAnswerMutation(
questionId: MaybeRefOrGetter<QuestionId>,
): UseMutationReturnType<AnswerResponse, Error, number, unknown> {
return useMutation({
mutationFn: async (seq) => new AnswerController(toValue(questionId)).remove(seq),
});
}
export function useUpdateAnswerMutation(
questionId: MaybeRefOrGetter<QuestionId>,
): UseMutationReturnType<AnswerResponse, Error, { answerData: AnswerData; seq: number }, unknown> {
return useMutation({
mutationFn: async (data, seq) => new AnswerController(toValue(questionId)).update(seq, data),
});
}

View file

@ -0,0 +1,93 @@
import { QuestionController, type QuestionResponse, type QuestionsResponse } from "@/controllers/questions.ts";
import type { QuestionData, QuestionId } from "@dwengo-1/common/interfaces/question";
import type { LearningObjectIdentifierDTO } from "@dwengo-1/common/interfaces/learning-content";
import { computed, type MaybeRefOrGetter, toValue } from "vue";
import {
useMutation,
type UseMutationReturnType,
useQuery,
useQueryClient,
type UseQueryReturnType,
} from "@tanstack/vue-query";
export function questionsQueryKey(
loId: LearningObjectIdentifierDTO,
full: boolean,
): [string, string, number, string, boolean] {
return ["questions", loId.hruid, loId.version, loId.language, full];
}
export function questionQueryKey(questionId: QuestionId): [string, string, number, string, number] {
const loId = questionId.learningObjectIdentifier;
return ["question", loId.hruid, loId.version, loId.language, questionId.sequenceNumber];
}
export function useQuestionsQuery(
loId: MaybeRefOrGetter<LearningObjectIdentifierDTO>,
full: MaybeRefOrGetter<boolean> = true,
): UseQueryReturnType<QuestionsResponse, Error> {
return useQuery({
queryKey: computed(() => questionsQueryKey(toValue(loId), toValue(full))),
queryFn: async () => new QuestionController(toValue(loId)).getAll(toValue(full)),
enabled: () => Boolean(toValue(loId)),
});
}
export function useQuestionQuery(
questionId: MaybeRefOrGetter<QuestionId>,
): UseQueryReturnType<QuestionResponse, Error> {
const loId = toValue(questionId).learningObjectIdentifier;
const sequenceNumber = toValue(questionId).sequenceNumber;
return useQuery({
queryKey: computed(() => questionQueryKey(loId, sequenceNumber)),
queryFn: async () => new QuestionController(loId).getBy(sequenceNumber),
enabled: () => Boolean(toValue(questionId)),
});
}
export function useCreateQuestionMutation(
loId: MaybeRefOrGetter<LearningObjectIdentifierDTO>,
): UseMutationReturnType<QuestionResponse, Error, QuestionData, unknown> {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (data) => new QuestionController(toValue(loId)).create(data),
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: questionsQueryKey(toValue(loId), true) });
await queryClient.invalidateQueries({ queryKey: questionsQueryKey(toValue(loId), false) });
},
});
}
export function useUpdateQuestionMutation(
questionId: MaybeRefOrGetter<QuestionId>,
): UseMutationReturnType<QuestionResponse, Error, QuestionData, unknown> {
const queryClient = useQueryClient();
const loId = toValue(questionId).learningObjectIdentifier;
const sequenceNumber = toValue(questionId).sequenceNumber;
return useMutation({
mutationFn: async (data) => new QuestionController(loId).update(sequenceNumber, data),
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: questionsQueryKey(toValue(loId), true) });
await queryClient.invalidateQueries({ queryKey: questionsQueryKey(toValue(loId), false) });
await queryClient.invalidateQueries({ queryKey: questionQueryKey(toValue(questionId)) });
},
});
}
export function useDeleteQuestionMutation(
questionId: MaybeRefOrGetter<QuestionId>,
): UseMutationReturnType<QuestionResponse, Error, void, unknown> {
const queryClient = useQueryClient();
const loId = toValue(questionId).learningObjectIdentifier;
const sequenceNumber = toValue(questionId).sequenceNumber;
return useMutation({
mutationFn: async () => new QuestionController(loId).remove(sequenceNumber),
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: questionsQueryKey(toValue(loId), true) });
await queryClient.invalidateQueries({ queryKey: questionsQueryKey(toValue(loId), false) });
await queryClient.invalidateQueries({ queryKey: questionQueryKey(toValue(questionId)) });
},
});
}

View file

@ -19,7 +19,7 @@ import type { AssignmentsResponse } from "@/controllers/assignments.ts";
import type { GroupsResponse } from "@/controllers/groups.ts";
import type { SubmissionsResponse } from "@/controllers/submissions.ts";
import type { QuestionsResponse } from "@/controllers/questions.ts";
import type { StudentDTO } from "@dwengo-1/interfaces/student";
import type { StudentDTO } from "@dwengo-1/common/interfaces/student";
const studentController = new StudentController();
@ -179,7 +179,7 @@ export function useCreateJoinRequestMutation(): UseMutationReturnType<
mutationFn: async ({ username, classId }) => studentController.createJoinRequest(username, classId),
onSuccess: async (newJoinRequest) => {
await queryClient.invalidateQueries({
queryKey: studentJoinRequestsQueryKey(newJoinRequest.request.requester),
queryKey: studentJoinRequestsQueryKey(newJoinRequest.request.requester.username),
});
},
});
@ -196,7 +196,7 @@ export function useDeleteJoinRequestMutation(): UseMutationReturnType<
return useMutation({
mutationFn: async ({ username, classId }) => studentController.deleteJoinRequest(username, classId),
onSuccess: async (deletedJoinRequest) => {
const username = deletedJoinRequest.request.requester;
const username = deletedJoinRequest.request.requester.username;
const classId = deletedJoinRequest.request.class;
await queryClient.invalidateQueries({ queryKey: studentJoinRequestsQueryKey(username) });
await queryClient.invalidateQueries({ queryKey: studentJoinRequestQueryKey(username, classId) });

View file

@ -1,11 +1,17 @@
import { computed, toValue } from "vue";
import type { MaybeRefOrGetter } from "vue";
import { useMutation, useQuery, useQueryClient, UseMutationReturnType, UseQueryReturnType } from "@tanstack/vue-query";
import {
useMutation,
useQuery,
useQueryClient,
type UseMutationReturnType,
type UseQueryReturnType,
} from "@tanstack/vue-query";
import { TeacherController, type TeacherResponse, type TeachersResponse } from "@/controllers/teachers.ts";
import type { ClassesResponse } from "@/controllers/classes.ts";
import type { JoinRequestResponse, JoinRequestsResponse, StudentsResponse } from "@/controllers/students.ts";
import type { QuestionsResponse } from "@/controllers/questions.ts";
import type { TeacherDTO } from "@dwengo-1/interfaces/teacher";
import type { TeacherDTO } from "@dwengo-1/common/interfaces/teacher";
import { studentJoinRequestQueryKey, studentJoinRequestsQueryKey } from "@/queries/students.ts";
const teacherController = new TeacherController();

View file

@ -1,7 +1,224 @@
<script setup lang="ts"></script>
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import authState from "@/services/auth/auth-service.ts";
import { onMounted, ref } from "vue";
import type { ClassDTO } from "@dwengo-1/common/interfaces/class";
import { useRoute } from "vue-router";
import { ClassController, type ClassResponse } from "@/controllers/classes";
import type { StudentsResponse } from "@/controllers/students";
import type { StudentDTO } from "@dwengo-1/common/interfaces/student";
const { t } = useI18n();
// Username of logged in teacher
const username = ref<string | undefined>(undefined);
const classController: ClassController = new ClassController();
// Find class id from route
const route = useRoute();
const classId: string = route.params.id as string;
const isLoading = ref(true);
const currentClass = ref<ClassDTO | undefined>(undefined);
const students = ref<StudentDTO[]>([]);
// Find the username of the logged in user so it can be used to fetch other information
// When loading the page
onMounted(async () => {
const userObject = await authState.loadUser();
username.value = userObject?.profile?.preferred_username ?? undefined;
// Get class of which information should be shown
const classResponse: ClassResponse = await classController.getById(classId);
if (classResponse && classResponse.class) {
currentClass.value = classResponse.class;
isLoading.value = false;
}
// Fetch all students of the class
const studentsResponse: StudentsResponse = await classController.getStudents(classId);
if (studentsResponse && studentsResponse.students) students.value = studentsResponse.students as StudentDTO[];
});
// TODO: Boolean that handles visibility for dialogs
// Popup to verify removing student
const dialog = ref(false);
const selectedStudent = ref<StudentDTO | null>(null);
function showPopup(s: StudentDTO): void {
selectedStudent.value = s;
dialog.value = true;
}
// Remove student from class
function removeStudentFromclass(): void {
dialog.value = false;
}
</script>
<template>
<main></main>
</template>
<main>
<div
v-if="isLoading"
class="text-center py-10"
>
<v-progress-circular
indeterminate
color="primary"
/>
<p>Loading...</p>
</div>
<div v-else>
<h1 class="title">{{ currentClass!.displayName }}</h1>
<v-container
fluid
class="ma-4"
>
<v-row
no-gutters
fluid
>
<v-col
cols="12"
sm="6"
md="6"
>
<v-table class="table">
<thead>
<tr>
<th class="header">{{ t("students") }}</th>
<th class="header"></th>
</tr>
</thead>
<tbody>
<tr
v-for="s in students"
:key="s.id"
>
<td>
{{ s.firstName + " " + s.lastName }}
</td>
<td>
<v-btn @click="showPopup"> {{ t("remove") }} </v-btn>
</td>
</tr>
</tbody>
</v-table>
</v-col>
</v-row>
</v-container>
</div>
<v-dialog
v-model="dialog"
max-width="400px"
>
<v-card>
<v-card-title class="headline">{{ t("areusure") }}</v-card-title>
<style scoped></style>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
text
@click="dialog = false"
>
{{ t("cancel") }}
</v-btn>
<v-btn
text
@click="removeStudentFromclass"
>{{ t("yes") }}</v-btn
>
</v-card-actions>
</v-card>
</v-dialog>
</main>
</template>
<style scoped>
.header {
font-weight: bold !important;
background-color: #0e6942;
color: white;
padding: 10px;
}
table thead th:first-child {
border-top-left-radius: 10px;
}
.table thead th:last-child {
border-top-right-radius: 10px;
}
.table tbody tr:nth-child(odd) {
background-color: white;
}
.table tbody tr:nth-child(even) {
background-color: #f6faf2;
}
td,
th {
border-bottom: 1px solid #0e6942;
border-top: 1px solid #0e6942;
}
.table {
width: 90%;
padding-top: 10px;
border-collapse: collapse;
}
h1 {
color: #0e6942;
text-transform: uppercase;
font-weight: bolder;
padding-top: 2%;
font-size: 50px;
}
h2 {
color: #0e6942;
font-size: 30px;
}
.join {
display: flex;
flex-direction: column;
gap: 20px;
margin-top: 50px;
}
.link {
color: #0b75bb;
text-decoration: underline;
}
main {
margin-left: 30px;
}
@media screen and (max-width: 800px) {
h1 {
text-align: center;
padding-left: 0;
}
.join {
text-align: center;
align-items: center;
margin-left: 0;
}
.sheet {
width: 100%;
}
main {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 5px;
}
}
</style>

View file

@ -1,7 +1,380 @@
<script setup lang="ts"></script>
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import authState from "@/services/auth/auth-service.ts";
import { computed, onMounted, ref, type ComputedRef } from "vue";
import { validate, version } from "uuid";
import type { ClassDTO } from "@dwengo-1/common/interfaces/class";
import { useCreateJoinRequestMutation, useStudentClassesQuery } from "@/queries/students";
import type { StudentDTO } from "@dwengo-1/common/interfaces/student";
import { StudentController } from "@/controllers/students";
import type { TeacherDTO } from "@dwengo-1/common/interfaces/teacher";
import { TeacherController } from "@/controllers/teachers";
const { t } = useI18n();
const studentController: StudentController = new StudentController();
const teacherController: TeacherController = new TeacherController();
// Username of logged in student
const username = ref<string | undefined>(undefined);
// Find the username of the logged in user so it can be used to fetch other information
// When loading the page
onMounted(async () => {
const userObject = await authState.loadUser();
username.value = userObject?.profile?.preferred_username ?? undefined;
});
// Fetch all classes of the logged in student
const { data: classesResponse, isLoading, error } = useStudentClassesQuery(username);
// Empty list when classes are not yet loaded, else the list of classes of the user
const classes: ComputedRef<ClassDTO[]> = computed(() => {
// The classes are not yet fetched
if (!classesResponse.value) {
return [];
}
// The user has no classes
if (classesResponse.value.classes.length === 0) {
return [];
}
return classesResponse.value.classes as ClassDTO[];
});
// Students of selected class are shown when logged in student presses on the member count
const selectedClass = ref<ClassDTO | null>(null);
const students = ref<StudentDTO[]>([]);
const teachers = ref<TeacherDTO[]>([]);
const getStudents = ref(false);
// Boolean that handles visibility for dialogs
// Clicking on membercount will show a dialog with all members
const dialog = ref(false);
// Function to display all members of a class in a dialog
async function openStudentDialog(c: ClassDTO): Promise<void> {
selectedClass.value = c;
// Clear previous value
getStudents.value = true;
students.value = [];
dialog.value = true;
// Fetch students from their usernames to display their full names
const studentDTOs: (StudentDTO | null)[] = await Promise.all(
c.students.map(async (uid) => {
try {
const res = await studentController.getByUsername(uid);
return res.student;
} catch (_) {
return null;
}
}),
);
// Only show students that are not fetched ass *null*
students.value = studentDTOs.filter(Boolean) as StudentDTO[];
}
async function openTeacherDialog(c: ClassDTO): Promise<void> {
selectedClass.value = c;
// Clear previous value
getStudents.value = false;
teachers.value = [];
dialog.value = true;
// Fetch names of teachers
const teacherDTOs: (TeacherDTO | null)[] = await Promise.all(
c.teachers.map(async (uid) => {
try {
const res = await teacherController.getByUsername(uid);
return res.teacher;
} catch (_) {
return null;
}
}),
);
teachers.value = teacherDTOs.filter(Boolean) as TeacherDTO[];
}
// Hold the code a student gives in to join a class
const code = ref<string>("");
// 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
const codeRules = [
(value: string | undefined): string | boolean => {
if (value === undefined || value === "") {
return true;
} else if (value !== undefined && validate(value) && version(value) === 4) {
return true;
}
return t("invalidFormat");
},
];
// Used to send the actual class join request
const { mutate } = useCreateJoinRequestMutation();
// 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) {
mutate(
{ username: username.value!, classId: code.value },
{
onSuccess: () => {
showSnackbar(t("sent"), "success");
},
onError: (e) => {
showSnackbar(t("failed") + ": " + e.message, "error");
},
},
);
code.value = "";
}
}
const snackbar = ref({
visible: false,
message: "",
color: "success",
});
function showSnackbar(message: string, color: string): void {
snackbar.value.message = message;
snackbar.value.color = color;
snackbar.value.visible = true;
}
</script>
<template>
<main></main>
</template>
<main>
<div
v-if="isLoading"
class="text-center py-10"
>
<v-progress-circular
indeterminate
color="primary"
/>
<p>Loading...</p>
</div>
<style scoped></style>
<div
v-else-if="error"
class="text-center py-10 text-error"
>
<v-icon large>mdi-alert-circle</v-icon>
<p>Error loading: {{ error.message }}</p>
</div>
<div v-else>
<h1 class="title">{{ t("classes") }}</h1>
<v-container
fluid
class="ma-4"
>
<v-row
no-gutters
fluid
>
<v-col
cols="12"
sm="6"
md="6"
>
<v-table class="table">
<thead>
<tr>
<th class="header">{{ t("classes") }}</th>
<th class="header">{{ t("teachers") }}</th>
<th class="header">{{ t("members") }}</th>
</tr>
</thead>
<tbody>
<tr
v-for="c in classes"
:key="c.id"
>
<td>{{ c.displayName }}</td>
<td
class="link"
@click="openTeacherDialog(c)"
>
{{ c.teachers.length }}
</td>
<td
class="link"
@click="openStudentDialog(c)"
>
{{ c.students.length }}
</td>
</tr>
</tbody>
</v-table>
</v-col>
</v-row>
</v-container>
<v-dialog
v-model="dialog"
width="400"
>
<v-card>
<v-card-title> {{ selectedClass?.displayName }} </v-card-title>
<v-card-text>
<ul v-if="getStudents">
<li
v-for="student in students"
:key="student.username"
>
{{ student.firstName + " " + student.lastName }}
</li>
</ul>
<ul v-else>
<li
v-for="teacher in teachers"
:key="teacher.username"
>
{{ teacher.firstName + " " + teacher.lastName }}
</li>
</ul>
</v-card-text>
<v-card-actions>
<v-btn
color="primary"
@click="dialog = false"
>Close</v-btn
>
</v-card-actions>
</v-card>
</v-dialog>
<div>
<div class="join">
<h2>{{ t("joinClass") }}</h2>
<p>{{ t("JoinClassExplanation") }}</p>
<v-sheet
class="pa-4 sheet"
max-width="400"
>
<v-form @submit.prevent>
<v-text-field
label="CODE"
v-model="code"
placeholder="XXXXXXXX-XXXX-4XXX-XXXX-XXXXXXXXXXXX"
:rules="codeRules"
variant="outlined"
></v-text-field>
<v-btn
class="mt-4"
color="#f6faf2"
type="submit"
@click="submitCode"
block
>{{ t("submitCode") }}</v-btn
>
</v-form>
</v-sheet>
</div>
</div>
</div>
<v-snackbar
v-model="snackbar.visible"
:color="snackbar.color"
timeout="3000"
>
{{ snackbar.message }}
</v-snackbar>
</main>
</template>
<style scoped>
.header {
font-weight: bold !important;
background-color: #0e6942;
color: white;
padding: 10px;
}
table thead th:first-child {
border-top-left-radius: 10px;
}
.table thead th:last-child {
border-top-right-radius: 10px;
}
.table tbody tr:nth-child(odd) {
background-color: white;
}
.table tbody tr:nth-child(even) {
background-color: #f6faf2;
}
td,
th {
border-bottom: 1px solid #0e6942;
border-top: 1px solid #0e6942;
}
.table {
width: 90%;
padding-top: 10px;
border-collapse: collapse;
}
h1 {
color: #0e6942;
text-transform: uppercase;
font-weight: bolder;
padding-top: 2%;
font-size: 50px;
}
h2 {
color: #0e6942;
font-size: 30px;
}
.join {
display: flex;
flex-direction: column;
gap: 20px;
margin-top: 50px;
}
.link {
color: #0b75bb;
text-decoration: underline;
}
main {
margin-left: 30px;
}
@media screen and (max-width: 800px) {
h1 {
text-align: center;
padding-left: 0;
}
.join {
text-align: center;
align-items: center;
margin-left: 0;
}
.sheet {
width: 100%;
}
main {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 5px;
}
}
</style>

View file

@ -1,7 +1,404 @@
<script setup lang="ts"></script>
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import authState from "@/services/auth/auth-service.ts";
import { computed, onMounted, ref, type ComputedRef } from "vue";
import type { TeacherDTO } from "@dwengo-1/common/interfaces/teacher";
import type { ClassDTO } from "@dwengo-1/common/interfaces/class";
import type { TeacherInvitationDTO } from "@dwengo-1/common/interfaces/teacher-invitation";
import { useTeacherClassesQuery } from "@/queries/teachers";
import { ClassController, type ClassResponse } from "@/controllers/classes";
const { t } = useI18n();
const classController = new ClassController();
// Username of logged in teacher
const username = ref<string | undefined>(undefined);
// Find the username of the logged in user so it can be used to fetch other information
// When loading the page
onMounted(async () => {
const userObject = await authState.loadUser();
username.value = userObject?.profile?.preferred_username ?? undefined;
});
// Fetch all classes of the logged in teacher
const { data: classesResponse, isLoading, error, refetch } = useTeacherClassesQuery(username, true);
// Empty list when classes are not yet loaded, else the list of classes of the user
const classes: ComputedRef<ClassDTO[]> = computed(() => {
// The classes are not yet fetched
if (!classesResponse.value) {
return [];
}
// The user has no classes
if (classesResponse.value.classes.length === 0) {
return [];
}
return classesResponse.value.classes as ClassDTO[];
});
// Boolean that handles visibility for dialogs
// Creating a class will generate a popup with the generated code
const dialog = ref(false);
// Code generated when new class was created
const code = ref<string>("");
// TODO: waiting on frontend controllers
const invitations = ref<TeacherInvitationDTO[]>([]);
// Function to handle a accepted invitation request
function acceptRequest(): void {
//TODO: avoid linting issues when merging by filling the function
invitations.value = [];
}
// Function to handle a denied invitation request
function denyRequest(): void {
//TODO: avoid linting issues when merging by filling the function
invitations.value = [];
}
// Teacher should be able to set a displayname when making a class
const className = ref<string>("");
// The name can only contain dash, underscore letters and numbers
// These rules are used to display a message to the user if the name is not valid
const nameRules = [
(value: string | undefined): string | boolean => {
if (!value) return true;
if (value && /^[a-zA-Z0-9_-]+$/.test(value)) return true;
return t("onlyUse");
},
];
// Function called when a teacher creates a class
async function createClass(): Promise<void> {
// Check if the class name is valid
if (className.value && className.value.length > 0 && /^[a-zA-Z0-9_-]+$/.test(className.value)) {
try {
const classDto: ClassDTO = {
id: "",
displayName: className.value,
teachers: [username.value!],
students: [],
joinRequests: [],
};
const classResponse: ClassResponse = await classController.createClass(classDto);
const createdClass: ClassDTO = classResponse.class;
code.value = createdClass.id;
dialog.value = true;
showSnackbar(t("created"), "success");
// Reload the table with classes so the new class appears
await refetch();
} catch (_) {
showSnackbar(t("wrong"), "error");
}
}
if (!className.value || className.value === "") {
showSnackbar(t("name is mandatory"), "error");
}
}
const snackbar = ref({
visible: false,
message: "",
color: "success",
});
function showSnackbar(message: string, color: string): void {
snackbar.value.message = message;
snackbar.value.color = color;
snackbar.value.visible = true;
}
// 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<void> {
await navigator.clipboard.writeText(code.value);
copied.value = true;
}
</script>
<template>
<main></main>
</template>
<main>
<div
v-if="isLoading"
class="text-center py-10"
>
<v-progress-circular
indeterminate
color="primary"
/>
<p>Loading...</p>
</div>
<style scoped></style>
<div
v-else-if="error"
class="text-center py-10 text-error"
>
<v-icon large>mdi-alert-circle</v-icon>
<p>Error loading: {{ error.message }}</p>
</div>
<div v-else>
<h1 class="title">{{ t("classes") }}</h1>
<v-container
fluid
class="ma-4"
>
<v-row
no-gutters
fluid
>
<v-col
cols="12"
sm="6"
md="6"
>
<v-table class="table">
<thead>
<tr>
<th class="header">{{ t("classes") }}</th>
<th class="header">
{{ t("code") }}
</th>
<th class="header">{{ t("members") }}</th>
</tr>
</thead>
<tbody>
<tr
v-for="c in classes"
:key="c.id"
>
<td>
<v-btn
:to="`/class/${c.id}`"
variant="text"
>
{{ c.displayName }}
<v-icon end> mdi-menu-right </v-icon>
</v-btn>
</td>
<td>{{ c.id }}</td>
<td>{{ c.students.length }}</td>
</tr>
</tbody>
</v-table>
</v-col>
<v-col
cols="12"
sm="6"
md="6"
>
<div>
<h2>{{ t("createClass") }}</h2>
<v-sheet
class="pa-4 sheet"
max-width="600px"
>
<p>{{ t("createClassInstructions") }}</p>
<v-form @submit.prevent>
<v-text-field
class="mt-4"
:label="`${t('classname')}`"
v-model="className"
:placeholder="`${t('EnterNameOfClass')}`"
:rules="nameRules"
variant="outlined"
></v-text-field>
<v-btn
class="mt-4"
color="#f6faf2"
type="submit"
@click="createClass"
block
>{{ t("create") }}</v-btn
>
</v-form>
</v-sheet>
<v-container>
<v-dialog
v-model="dialog"
max-width="400px"
>
<v-card>
<v-card-title class="headline">code</v-card-title>
<v-card-text>
<v-text-field
v-model="code"
readonly
append-inner-icon="mdi-content-copy"
@click:append-inner="copyToClipboard"
></v-text-field>
<v-slide-y-transition>
<div
v-if="copied"
class="text-center mt-2"
>
{{ t("copied") }}
</div>
</v-slide-y-transition>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
text
@click="
dialog = false;
copied = false;
"
>
{{ t("close") }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-container>
</div>
</v-col>
</v-row>
</v-container>
<h1 class="title">
{{ t("invitations") }}
</h1>
<v-table class="table">
<thead>
<tr>
<th class="header">{{ t("class") }}</th>
<th class="header">{{ t("sender") }}</th>
<th class="header"></th>
</tr>
</thead>
<tbody>
<tr
v-for="i in invitations"
:key="(i.class as ClassDTO).id"
>
<td>
{{ (i.class as ClassDTO).displayName }}
</td>
<td>{{ (i.sender as TeacherDTO).firstName + " " + (i.sender as TeacherDTO).lastName }}</td>
<td class="text-right">
<div>
<v-btn
color="green"
@click="acceptRequest"
class="mr-2"
>
{{ t("accept") }}
</v-btn>
<v-btn
color="red"
@click="denyRequest"
>
{{ t("deny") }}
</v-btn>
</div>
</td>
</tr>
</tbody>
</v-table>
</div>
<v-snackbar
v-model="snackbar.visible"
:color="snackbar.color"
timeout="3000"
>
{{ snackbar.message }}
</v-snackbar>
</main>
</template>
<style scoped>
.header {
font-weight: bold !important;
background-color: #0e6942;
color: white;
padding: 10px;
}
table thead th:first-child {
border-top-left-radius: 10px;
}
.table thead th:last-child {
border-top-right-radius: 10px;
}
.table tbody tr:nth-child(odd) {
background-color: white;
}
.table tbody tr:nth-child(even) {
background-color: #f6faf2;
}
td,
th {
border-bottom: 1px solid #0e6942;
border-top: 1px solid #0e6942;
}
.table {
width: 90%;
padding-top: 10px;
border-collapse: collapse;
}
h1 {
color: #0e6942;
text-transform: uppercase;
font-weight: bolder;
padding-top: 2%;
font-size: 50px;
}
h2 {
color: #0e6942;
font-size: 30px;
}
.join {
display: flex;
flex-direction: column;
gap: 20px;
margin-top: 50px;
}
.link {
color: #0b75bb;
text-decoration: underline;
}
main {
margin-left: 30px;
}
@media screen and (max-width: 800px) {
h1 {
text-align: center;
padding-left: 0;
}
.join {
text-align: center;
align-items: center;
margin-left: 0;
}
.sheet {
width: 100%;
}
main {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 5px;
}
}
</style>

View file

@ -1,7 +1,17 @@
<script setup lang="ts"></script>
<script setup lang="ts">
import authState from "@/services/auth/auth-service.ts";
import TeacherClasses from "./TeacherClasses.vue";
import StudentClasses from "./StudentClasses.vue";
// Determine if role is student or teacher to render correct view
const role: string = authState.authState.activeRole!;
</script>
<template>
<main></main>
<main>
<TeacherClasses v-if="role === 'teacher'"></TeacherClasses>
<StudentClasses v-else></StudentClasses>
</main>
</template>
<style scoped></style>