Merge remote-tracking branch 'origin/feat/pagina-overzicht-klassen-voor-student' into feat/pagina-overzicht-klassen-voor-student
# Conflicts: # frontend/src/controllers/students.ts
This commit is contained in:
		
						commit
						65256d1691
					
				
					 4 changed files with 711 additions and 508 deletions
				
			
		|  | @ -19,7 +19,7 @@ import type { AssignmentsResponse } from "@/controllers/assignments.ts"; | ||||||
| import type { GroupsResponse } from "@/controllers/groups.ts"; | import type { GroupsResponse } from "@/controllers/groups.ts"; | ||||||
| import type { SubmissionsResponse } from "@/controllers/submissions.ts"; | import type { SubmissionsResponse } from "@/controllers/submissions.ts"; | ||||||
| import type { QuestionsResponse } from "@/controllers/questions.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(); | const studentController = new StudentController(); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										311
									
								
								frontend/src/views/classes/StudentClasses.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										311
									
								
								frontend/src/views/classes/StudentClasses.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,311 @@ | ||||||
|  | <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"; | ||||||
|  | 
 | ||||||
|  |     const { t } = useI18n(); | ||||||
|  |     const studentController: StudentController = new StudentController(); | ||||||
|  | 
 | ||||||
|  |     // 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 []; | ||||||
|  |         } | ||||||
|  |         if (typeof classesResponse.value.classes[0] === "string") { | ||||||
|  |             // Should not occur because value of *full* is true | ||||||
|  |             // Must be caught because typescript can't know the type | ||||||
|  |             // I chose to return an empty list if this occurs | ||||||
|  |             // It is also possible to fetch all classes from the id's returned | ||||||
|  |             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[]>([]); | ||||||
|  | 
 | ||||||
|  |     // 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 openDialog(c: ClassDTO) : Promise<void> { | ||||||
|  |         selectedClass.value = c; | ||||||
|  | 
 | ||||||
|  |         // Clear previous value | ||||||
|  |         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[]; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // 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 && validate(value) && version(value) === 4) return true; | ||||||
|  |             return t("invalidFormat"); | ||||||
|  |         }, | ||||||
|  |     ]; | ||||||
|  | 
 | ||||||
|  |     // Used to send the actual class join request | ||||||
|  |     const { mutate, isError } = useCreateJoinRequestMutation(); | ||||||
|  | 
 | ||||||
|  |     // Function called when a student submits a code to join a class | ||||||
|  |     function submitCode() { | ||||||
|  |         // Check if the code is valid | ||||||
|  |         if (code.value !== undefined && validate(code.value) && version(code.value) === 4) { | ||||||
|  |             mutate( { username : username.value! , classId : code.value }); | ||||||
|  | 
 | ||||||
|  |             console.log("Code submitted:", code.value); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | </script> | ||||||
|  | <template> | ||||||
|  |     <main> | ||||||
|  |         <div | ||||||
|  |             v-if="isLoading" | ||||||
|  |             class="text-center py-10" | ||||||
|  |         > | ||||||
|  |             <v-progress-circular | ||||||
|  |                 indeterminate | ||||||
|  |                 color="primary" | ||||||
|  |             /> | ||||||
|  |             <p>Loading...</p> | ||||||
|  |         </div> | ||||||
|  | 
 | ||||||
|  |         <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("members") }}</th> | ||||||
|  |                                 </tr> | ||||||
|  |                             </thead> | ||||||
|  |                             <tbody> | ||||||
|  |                                 <tr | ||||||
|  |                                     v-for="c in classes" | ||||||
|  |                                     :key="c.id" | ||||||
|  |                                 > | ||||||
|  |                                     <td>{{ c.displayName }}</td> | ||||||
|  |                                     <td | ||||||
|  |                                         class="link" | ||||||
|  |                                         @click="openDialog(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> | ||||||
|  |                             <li | ||||||
|  |                                 v-for="student in students" | ||||||
|  |                                 :key="student.username" | ||||||
|  |                             > | ||||||
|  |                                 {{ student.firstName + " " + student.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> | ||||||
|  |     </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> | ||||||
							
								
								
									
										392
									
								
								frontend/src/views/classes/TeacherClasses.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										392
									
								
								frontend/src/views/classes/TeacherClasses.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,392 @@ | ||||||
|  | <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"; | ||||||
|  | 
 | ||||||
|  |     const { t } = useI18n(); | ||||||
|  | 
 | ||||||
|  |     // TODO: remove and use correct type | ||||||
|  |     interface Invitation { | ||||||
|  |         id: string; | ||||||
|  |         class: ClassDTO; | ||||||
|  |         sender: TeacherDTO; | ||||||
|  |         receiver: TeacherDTO; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // 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; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     // TODO: fetch all classes of the logged in teacher | ||||||
|  |     const isLoading = ref(false); | ||||||
|  |     const error = ref<Error | null>(null); | ||||||
|  |     // 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(() =>  | ||||||
|  |          [] | ||||||
|  |         // TODO | ||||||
|  |         // // the classes are not yet fetched | ||||||
|  |         // If (!classesResponse.value) { | ||||||
|  |         //     Return []; | ||||||
|  |         // } | ||||||
|  |         //     // the user has no classes | ||||||
|  |         //     If (classesResponse.value.classes.length === 0) { | ||||||
|  |         //         Return []; | ||||||
|  |         //     } | ||||||
|  |         //     If (typeof classesResponse.value.classes[0] === "string") { | ||||||
|  |         //         // should not occur because value of *full* is true | ||||||
|  |         //         // must be caught because typescript can't know the type | ||||||
|  |         //         // i chose to return an empty list if this occurs | ||||||
|  |         //         // it is also possible to fetch all classes from the id's returned | ||||||
|  |         //         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); | ||||||
|  | 
 | ||||||
|  |     // Duntion to display the dialog showing generated code for created class | ||||||
|  |     function openDialog() : void { | ||||||
|  |         //TODO | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Code generated when new class was created | ||||||
|  |     const code = ref<string>(""); | ||||||
|  | 
 | ||||||
|  |     // TODO: implement correctly | ||||||
|  |     const invitations = ref<Invitation[]>([]); | ||||||
|  | 
 | ||||||
|  |     // Function to handle a accepted invitation request | ||||||
|  |     function acceptRequest() { | ||||||
|  |         //TODO | ||||||
|  |         console.log("request accepted"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Function to handle a denied invitation request | ||||||
|  |     function denyRequest() { | ||||||
|  |         //TODO | ||||||
|  |         console.log("request denied"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // 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) => { | ||||||
|  |             if (value) return true; | ||||||
|  |             return t("nameIsMandatory"); | ||||||
|  |         }, | ||||||
|  |         (value: string | undefined) => { | ||||||
|  |             if (value && /^[a-zA-Z0-9_-]+$/.test(value)) return true; | ||||||
|  |             return t("onlyUse"); | ||||||
|  |         }, | ||||||
|  |     ]; | ||||||
|  | 
 | ||||||
|  |     // Function called when a teacher creates a class | ||||||
|  |     function createClass() { | ||||||
|  |         // Check if the class name is valid | ||||||
|  |         if (className.value && className.value.length > 0 && /^[a-zA-Z0-9_-]+$/.test(className.value)) { | ||||||
|  |             //TODO | ||||||
|  |             console.log("created class with name: " + className.value); | ||||||
|  | 
 | ||||||
|  |             // Show the generated code to share with the class | ||||||
|  |             dialog.value = true; | ||||||
|  |             code.value = "04c7c759-c41e-4ea9-968a-1e2a987ce0ed"; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Show the teacher, copying of the code was a successs | ||||||
|  |     const copied = ref(false); | ||||||
|  | 
 | ||||||
|  |     // Copy the generated code to the clipboard | ||||||
|  |     function copyToClipboard() { | ||||||
|  |         navigator.clipboard.writeText(code.value); | ||||||
|  |         copied.value = true; | ||||||
|  |     } | ||||||
|  | </script> | ||||||
|  | <template> | ||||||
|  |     <main> | ||||||
|  |         <div | ||||||
|  |             v-if="isLoading" | ||||||
|  |             class="text-center py-10" | ||||||
|  |         > | ||||||
|  |             <v-progress-circular | ||||||
|  |                 indeterminate | ||||||
|  |                 color="primary" | ||||||
|  |             /> | ||||||
|  |             <p>Loading...</p> | ||||||
|  |         </div> | ||||||
|  | 
 | ||||||
|  |         <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="`/user/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.id" | ||||||
|  |                     > | ||||||
|  |                         <td> | ||||||
|  |                             {{ i.class.displayName }} | ||||||
|  |                         </td> | ||||||
|  |                         <td>{{ i.sender.firstName + " " + i.sender.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> | ||||||
|  |     </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> | ||||||
|  | @ -1,517 +1,17 @@ | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
|     import { useI18n } from "vue-i18n"; |  | ||||||
|     import authState from "@/services/auth/auth-service.ts"; |     import authState from "@/services/auth/auth-service.ts"; | ||||||
|     import { computed, onMounted, ref, type ComputedRef } from "vue"; |     import TeacherClasses from "./TeacherClasses.vue"; | ||||||
|     import { validate, version } from "uuid"; |     import StudentClasses from "./StudentClasses.vue"; | ||||||
|     import type { TeacherDTO } from "@dwengo-1/common/interfaces/teacher"; |  | ||||||
|     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"; |  | ||||||
| 
 | 
 | ||||||
|     const { t } = useI18n(); |     // Determine if role is student or teacher to render correct view | ||||||
|     const studentController: StudentController = new StudentController(); |  | ||||||
| 
 |  | ||||||
|     // TODO: remove and use correct type |  | ||||||
|     interface Invitation { |  | ||||||
|         id: string; |  | ||||||
|         class: ClassDTO; |  | ||||||
|         sender: TeacherDTO; |  | ||||||
|         receiver: TeacherDTO; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // determine if role is student or teacher to render correct view |  | ||||||
|     const role: string = authState.authState.activeRole!; |     const role: string = authState.authState.activeRole!; | ||||||
| 
 |  | ||||||
|     // username of logged in student or 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 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 []; |  | ||||||
|             } |  | ||||||
|             if (typeof classesResponse.value.classes[0] === "string") { |  | ||||||
|                 // should not occur because value of *full* is true |  | ||||||
|                 // must be caught because typescript can't know the type |  | ||||||
|                 // i chose to return an empty list if this occurs |  | ||||||
|                 // it is also possible to fetch all classes from the id's returned |  | ||||||
|                 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[]>([]); |  | ||||||
| 
 |  | ||||||
|     // Boolean that handles visibility for dialogs |  | ||||||
|     // For students: clicking on membercount will show a dialog with all members |  | ||||||
|     // For teachers: creating a class will generate a popup with the generated code |  | ||||||
|     const dialog = ref(false); |  | ||||||
| 
 |  | ||||||
|     // Function to display all members of a class in a dialog |  | ||||||
|     async function openDialog(c: ClassDTO) { |  | ||||||
|         selectedClass.value = c; |  | ||||||
| 
 |  | ||||||
|         // Clear previous value |  | ||||||
|         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[]; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // For students: code that they give in when sending a class join request |  | ||||||
|     // For teachers: code that they get when they create a new 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 && validate(value) && version(value) === 4) return true; |  | ||||||
|             return t("invalidFormat"); |  | ||||||
|         }, |  | ||||||
|     ]; |  | ||||||
| 
 |  | ||||||
|     const { mutate, isError } = useCreateJoinRequestMutation(); |  | ||||||
| 
 |  | ||||||
|     // Function called when a student submits a code to join a class |  | ||||||
|     function submitCode() { |  | ||||||
|         // Check if the code is valid |  | ||||||
|         if (code.value !== undefined && validate(code.value) && version(code.value) === 4) { |  | ||||||
|             // TODO: uncomment when fixed |  | ||||||
|             // mutate( { username : username.value! , classId : code.value }); |  | ||||||
|              |  | ||||||
|             console.log("Code submitted:", code.value); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // TODO: implement correctly |  | ||||||
|     const invitations = ref<Invitation[]>([]); |  | ||||||
| 
 |  | ||||||
|     // Function to handle a accepted invitation request |  | ||||||
|     function acceptRequest() { |  | ||||||
|         //TODO |  | ||||||
|         console.log("request accepted"); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // Function to handle a denied invitation request |  | ||||||
|     function denyRequest() { |  | ||||||
|         //TODO |  | ||||||
|         console.log("request denied"); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // 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) => { |  | ||||||
|             if (value) return true; |  | ||||||
|             return t("nameIsMandatory"); |  | ||||||
|         }, |  | ||||||
|         (value: string | undefined) => { |  | ||||||
|             if (value && /^[a-zA-Z0-9_-]+$/.test(value)) return true; |  | ||||||
|             return t("onlyUse"); |  | ||||||
|         }, |  | ||||||
|     ]; |  | ||||||
| 
 |  | ||||||
|     // Function called when a teacher creates a class |  | ||||||
|     function createClass() { |  | ||||||
|         // Check if the class name is valid |  | ||||||
|         if (className.value && className.value.length > 0 && /^[a-zA-Z0-9_-]+$/.test(className.value)) { |  | ||||||
|             //TODO |  | ||||||
|             console.log("created class with name: " + className.value); |  | ||||||
| 
 |  | ||||||
|             // Show the generated code to share with the class |  | ||||||
|             dialog.value = true; |  | ||||||
|             code.value = "04c7c759-c41e-4ea9-968a-1e2a987ce0ed"; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // show the teacher, copying of the code was a successs |  | ||||||
|     const copied = ref(false); |  | ||||||
| 
 |  | ||||||
|     // Copy the generated code to the clipboard |  | ||||||
|     function copyToClipboard() { |  | ||||||
|         navigator.clipboard.writeText(code.value); |  | ||||||
|         copied.value = true; |  | ||||||
|     } |  | ||||||
| </script> | </script> | ||||||
|  | 
 | ||||||
| <template> | <template> | ||||||
|     <main> |     <main> | ||||||
|         <div |         <TeacherClasses v-if="role === 'teacher'"></TeacherClasses> | ||||||
|             v-if="isLoading" |         <StudentClasses v-else></StudentClasses> | ||||||
|             class="text-center py-10" |  | ||||||
|         > |  | ||||||
|             <v-progress-circular |  | ||||||
|                 indeterminate |  | ||||||
|                 color="primary" |  | ||||||
|             /> |  | ||||||
|             <p>Loading...</p> |  | ||||||
|         </div> |  | ||||||
| 
 |  | ||||||
|         <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" |  | ||||||
|                                         v-if="role === 'teacher'" |  | ||||||
|                                     > |  | ||||||
|                                         {{ t("code") }} |  | ||||||
|                                     </th> |  | ||||||
|                                     <th class="header">{{ t("members") }}</th> |  | ||||||
|                                 </tr> |  | ||||||
|                             </thead> |  | ||||||
|                             <tbody> |  | ||||||
|                                 <tr |  | ||||||
|                                     v-for="c in classes" |  | ||||||
|                                     :key="c.id" |  | ||||||
|                                 > |  | ||||||
|                                     <td v-if="role === 'student'">{{ c.displayName }}</td> |  | ||||||
|                                     <td v-else> |  | ||||||
|                                         <v-btn |  | ||||||
|                                             :to="`/user/class/${c.id}`" |  | ||||||
|                                             variant="text" |  | ||||||
|                                         > |  | ||||||
|                                             {{ c.displayName }} |  | ||||||
|                                             <v-icon end> mdi-menu-right </v-icon> |  | ||||||
|                                         </v-btn> |  | ||||||
|                                     </td> |  | ||||||
|                                     <td v-if="role === 'teacher'">{{ c.id }}</td> |  | ||||||
|                                     <td |  | ||||||
|                                         v-if="role === 'student'" |  | ||||||
|                                         class="link" |  | ||||||
|                                         @click="openDialog(c)" |  | ||||||
|                                     > |  | ||||||
|                                         {{ c.students.length }} |  | ||||||
|                                     </td> |  | ||||||
|                                     <td v-else>{{ c.students.length }}</td> |  | ||||||
|                                 </tr> |  | ||||||
|                             </tbody> |  | ||||||
|                         </v-table> |  | ||||||
|                     </v-col> |  | ||||||
|                     <v-col |  | ||||||
|                         cols="12" |  | ||||||
|                         sm="6" |  | ||||||
|                         md="6" |  | ||||||
|                     > |  | ||||||
|                         <div v-if="role === 'teacher'"> |  | ||||||
|                             <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 |  | ||||||
|                 v-if="role === 'teacher'" |  | ||||||
|                 class="title" |  | ||||||
|             > |  | ||||||
|                 {{ t("invitations") }} |  | ||||||
|             </h1> |  | ||||||
|             <v-table |  | ||||||
|                 v-if="role === 'teacher'" |  | ||||||
|                 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.id" |  | ||||||
|                     > |  | ||||||
|                         <td> |  | ||||||
|                             {{ i.class.displayName }} |  | ||||||
|                         </td> |  | ||||||
|                         <td>{{ i.sender.firstName + " " + i.sender.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> |  | ||||||
| 
 |  | ||||||
|             <v-dialog |  | ||||||
|                 v-model="dialog" |  | ||||||
|                 width="400" |  | ||||||
|             > |  | ||||||
|                 <v-card> |  | ||||||
|                     <v-card-title> {{ selectedClass?.displayName }} </v-card-title> |  | ||||||
|                     <v-card-text> |  | ||||||
|                         <ul> |  | ||||||
|                             <li |  | ||||||
|                                 v-for="student in students" |  | ||||||
|                                 :key="student.username" |  | ||||||
|                             > |  | ||||||
|                                 {{ student.firstName + " " + student.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 v-if="role === 'student'"> |  | ||||||
|                 <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> |  | ||||||
|     </main> |     </main> | ||||||
| </template> | </template> | ||||||
| <style scoped> |  | ||||||
|     .header { |  | ||||||
|         font-weight: bold !important; |  | ||||||
|         background-color: #0e6942; |  | ||||||
|         color: white; |  | ||||||
|         padding: 10px; |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     table thead th:first-child { | <style scoped></style> | ||||||
|         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> |  | ||||||
|  |  | ||||||
		Reference in a new issue
	
	 Gabriellvl
						Gabriellvl