feat: drag and drop en random selection voor groepen
This commit is contained in:
		
							parent
							
								
									936a34b709
								
							
						
					
					
						commit
						a3185ed1c1
					
				
					 10 changed files with 4347 additions and 1249 deletions
				
			
		|  | @ -30,10 +30,9 @@ | ||||||
|     display: flex; |     display: flex; | ||||||
|     gap: 0.5rem; |     gap: 0.5rem; | ||||||
|     align-items: center; |     align-items: center; | ||||||
|     color: #0e6942 |     color: #0e6942; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| .group-section { | .group-section { | ||||||
|     margin-top: 2rem; |     margin-top: 2rem; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,7 +1,10 @@ | ||||||
| <template> | <template> | ||||||
|     <v-table class="table"> |     <v-table class="table"> | ||||||
|         <thead> |         <thead> | ||||||
|             <tr v-for="name in columns" :key="column"> |             <tr | ||||||
|  |                 v-for="name in columns" | ||||||
|  |                 :key="column" | ||||||
|  |             > | ||||||
|                 <th class="header">{{ name }}</th> |                 <th class="header">{{ name }}</th> | ||||||
|             </tr> |             </tr> | ||||||
|         </thead> |         </thead> | ||||||
|  | @ -38,12 +41,12 @@ | ||||||
| 
 | 
 | ||||||
| <script> | <script> | ||||||
|     export default { |     export default { | ||||||
|   name: 'columnList', |         name: "columnList", | ||||||
|         props: { |         props: { | ||||||
|             items: { |             items: { | ||||||
|                 type: Array, |                 type: Array, | ||||||
|       required: true |                 required: true, | ||||||
|     } |             }, | ||||||
|   } |         }, | ||||||
| } |     }; | ||||||
| </script> | </script> | ||||||
|  | @ -1,114 +1,438 @@ | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { ref, } from "vue"; |     import { computed, ref, watch } from "vue"; | ||||||
|     import draggable from "vuedraggable"; |     import draggable from "vuedraggable"; | ||||||
|     import { useI18n } from "vue-i18n"; |     import { useI18n } from "vue-i18n"; | ||||||
|  |     import { useClassStudentsQuery } from "@/queries/classes"; | ||||||
| 
 | 
 | ||||||
|     const props = defineProps<{ |     const props = defineProps<{ | ||||||
|         classId: string | undefined; |         classId: string | undefined; | ||||||
|     groups: string[][]; |         groups: object[]; | ||||||
|     }>(); |     }>(); | ||||||
| const emit = defineEmits(["done", "groupsUpdated"]); |     const emit = defineEmits(["close", "groupsUpdated", "done"]); | ||||||
|     const { t } = useI18n(); |     const { t } = useI18n(); | ||||||
| 
 | 
 | ||||||
| const groupList = ref(props.groups.map(g => [...g])); // deep copy |     interface StudentItem { | ||||||
| const unassigned = ref<string[]>([]); // voor vrije studenten |         username: string; | ||||||
|  |         fullName: string; | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|  |     const { data: studentsData } = useClassStudentsQuery(() => props.classId, true); | ||||||
|  | 
 | ||||||
|  |     // Dialog states | ||||||
|  |     const activeDialog = ref<"random" | "dragdrop" | null>(null); | ||||||
|  | 
 | ||||||
|  |     // Drag state | ||||||
|  |     const draggedItem = ref<{groupIndex: number, studentIndex: number} | null>(null); | ||||||
|  | 
 | ||||||
|  |     const currentGroups = ref<StudentItem[][]>([]); | ||||||
|  |     const unassignedStudents = ref<StudentItem[]>([]); | ||||||
|  |     const allStudents = ref<StudentItem[]>([]); | ||||||
|  | 
 | ||||||
|  |     // Random groups state | ||||||
|  |     const groupSize = ref(1); | ||||||
|  |     const randomGroupsPreview = ref<StudentItem[][]>([]); | ||||||
|  | 
 | ||||||
|  |     // Initialize data | ||||||
|  |     watch( | ||||||
|  |         () => [studentsData.value, props.groups], | ||||||
|  |         ([studentsVal, existingGroups]) => { | ||||||
|  |             if (!studentsVal) return; | ||||||
|  | 
 | ||||||
|  |             // Initialize all students | ||||||
|  |             allStudents.value = studentsVal.students.map((s) => ({ | ||||||
|  |                 username: s.username, | ||||||
|  |                 fullName: `${s.firstName} ${s.lastName}`, | ||||||
|  |             })); | ||||||
|  | 
 | ||||||
|  |             // Initialize groups if they exist | ||||||
|  |             if (existingGroups && existingGroups.length > 0) { | ||||||
|  |                 currentGroups.value = existingGroups.map((g) => [...g.members]); | ||||||
|  |                 const assignedUsernames = new Set( | ||||||
|  |                     existingGroups.flatMap((g) => g.members.map((m: StudentItem) => m.username)), | ||||||
|  |                 ); | ||||||
|  |                 unassignedStudents.value = allStudents.value.filter((s) => !assignedUsernames.has(s.username)); | ||||||
|  |             } else { | ||||||
|  |                 // Default to all students unassigned | ||||||
|  |                 currentGroups.value = []; | ||||||
|  |                 unassignedStudents.value = [...allStudents.value]; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // Initialize random preview with current groups | ||||||
|  |             randomGroupsPreview.value = [...currentGroups.value]; | ||||||
|  |         }, | ||||||
|  |         { immediate: true }, | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     // Random groups functions | ||||||
|  |     function generateRandomGroups() { | ||||||
|  |         if (groupSize.value < 1) return; | ||||||
|  | 
 | ||||||
|  |         // Shuffle students | ||||||
|  |         const shuffled = [...allStudents.value].sort(() => Math.random() - 0.5); | ||||||
|  | 
 | ||||||
|  |         // Create new groups | ||||||
|  |         const newGroups: StudentItem[][] = []; | ||||||
|  |         const groupCount = Math.ceil(shuffled.length / groupSize.value); | ||||||
|  | 
 | ||||||
|  |         for (let i = 0; i < groupCount; i++) { | ||||||
|  |             newGroups.push([]); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Distribute students | ||||||
|  |         shuffled.forEach((student, index) => { | ||||||
|  |             const groupIndex = index % groupCount; | ||||||
|  |             newGroups[groupIndex].push(student); | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         randomGroupsPreview.value = newGroups; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     function saveRandomGroups() { | ||||||
|  |         if (randomGroupsPreview.value.length === 0) { | ||||||
|  |             alert(t("please-generate-groups-first")); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         emit( | ||||||
|  |             "groupsUpdated", | ||||||
|  |             randomGroupsPreview.value.map((g) => g.map((s) => s.username)), | ||||||
|  |         ); | ||||||
|  |         activeDialog.value = null; | ||||||
|  |         emit("done"); | ||||||
|  |         emit("close"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Drag and drop functions | ||||||
|     function addNewGroup() { |     function addNewGroup() { | ||||||
|     groupList.value.push([]); |         currentGroups.value.push([]); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     function removeGroup(index: number) { |     function removeGroup(index: number) { | ||||||
|     unassigned.value.push(...groupList.value[index]); |         // Move students back to unassigned | ||||||
|     groupList.value.splice(index, 1); |         unassignedStudents.value.push(...currentGroups.value[index]); | ||||||
|  |         currentGroups.value.splice(index, 1); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| function saveChanges() { |     // Native Drag & Drop Handlers | ||||||
|     emit("groupsUpdated", groupList.value); |     function handleDragStart(groupIndex: number, studentIndex: number) { | ||||||
|  |         draggedItem.value = { groupIndex, studentIndex }; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     function handleDragOver(e: DragEvent, _: number) { | ||||||
|  |         e.preventDefault(); | ||||||
|  |         e.dataTransfer!.dropEffect = "move"; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     function handleDrop(e: DragEvent, targetGroupIndex: number, targetStudentIndex?: number) { | ||||||
|  |         e.preventDefault(); | ||||||
|  |         if (!draggedItem.value) return; | ||||||
|  | 
 | ||||||
|  |         const { groupIndex: sourceGroupIndex, studentIndex: sourceStudentIndex } = draggedItem.value; | ||||||
|  |         const isSameGroup = sourceGroupIndex === targetGroupIndex; | ||||||
|  | 
 | ||||||
|  |         let sourceArray, targetArray; | ||||||
|  | 
 | ||||||
|  |         // Determine source and target arrays | ||||||
|  |         if (sourceGroupIndex === -1) { | ||||||
|  |             sourceArray = unassignedStudents.value; | ||||||
|  |         } else { | ||||||
|  |             sourceArray = currentGroups.value[sourceGroupIndex]; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (targetGroupIndex === -1) { | ||||||
|  |             targetArray = unassignedStudents.value; | ||||||
|  |         } else { | ||||||
|  |             targetArray = currentGroups.value[targetGroupIndex]; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Remove from source | ||||||
|  |         const [movedStudent] = sourceArray.splice(sourceStudentIndex, 1); | ||||||
|  | 
 | ||||||
|  |         // Add to target | ||||||
|  |         if (targetStudentIndex !== undefined) { | ||||||
|  |             targetArray.splice(targetStudentIndex, 0, movedStudent); | ||||||
|  |         } else { | ||||||
|  |             targetArray.push(movedStudent); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         draggedItem.value = null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     function saveDragDrop() { | ||||||
|  |         if (unassignedStudents.value.length > 0) { | ||||||
|  |             alert(t("please-assign-all-students")); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         emit( | ||||||
|  |             "groupsUpdated", | ||||||
|  |             currentGroups.value.map((g) => g.map((s) => s.username)), | ||||||
|  |         ); | ||||||
|  |         activeDialog.value = null; | ||||||
|         emit("done"); |         emit("done"); | ||||||
|  |         emit("close"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Preview current groups in the main view | ||||||
|  |     const showGroupsPreview = computed(() => { | ||||||
|  |         return currentGroups.value.length > 0 || unassignedStudents.value.length > 0; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     function removeStudent(groupIndex, student) { | ||||||
|  |         const group = currentGroups.value[groupIndex]; | ||||||
|  |         currentGroups.value[groupIndex] = group.filter((s) => s.username !== student.username); | ||||||
|  |         unassignedStudents.value.push(student); | ||||||
|     } |     } | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|     <v-card> |     <v-card class="pa-4"> | ||||||
|         <v-card-title>{{ t("edit-groups") }}</v-card-title> |         <!-- Current Groups Preview --> | ||||||
|         <v-card-text> |         <div | ||||||
|             <v-row> |             v-if="showGroupsPreview" | ||||||
|                 <!-- Ongegroepeerde studenten --> |             class="mb-6" | ||||||
|                 <v-col cols="12" sm="4"> |  | ||||||
|                     <h4>{{ t("unassigned") }}</h4> |  | ||||||
|                     <draggable |  | ||||||
|                         v-model="unassigned" |  | ||||||
|                         group="students" |  | ||||||
|                         item-key="username" |  | ||||||
|                         class="group-box" |  | ||||||
|         > |         > | ||||||
|                         <template #item="{ element }"> |             <h3 class="mb-2">{{ t("current-groups") }}</h3> | ||||||
|                             <v-chip>{{ element }}</v-chip> |             <div | ||||||
|                         </template> |                 v-for="(group, index) in currentGroups" | ||||||
|                     </draggable> |                 :key="'preview-' + index" | ||||||
|                 </v-col> |                 class="mb-3" | ||||||
|  |             > | ||||||
|  |                 <div class="d-flex align-center"> | ||||||
|  |                     <strong class="mr-2">{{ t("group") }} {{ index + 1 }}:</strong> | ||||||
|  |                     <span class="text-caption">({{ group.length }} {{ t("members") }})</span> | ||||||
|  |                 </div> | ||||||
|  |                 <div class="d-flex flex-wrap"> | ||||||
|  |                     <v-chip | ||||||
|  |                         v-for="student in group" | ||||||
|  |                         :key="student.username" | ||||||
|  |                         class="ma-1" | ||||||
|  |                     > | ||||||
|  |                         {{ student.fullName }} | ||||||
|  |                     </v-chip> | ||||||
|  |                 </div> | ||||||
|  |             </div> | ||||||
| 
 | 
 | ||||||
|                 <!-- Bestaande groepen --> |             <div | ||||||
|                 <v-col |                 v-if="unassignedStudents.length > 0" | ||||||
|                     v-for="(group, i) in groupList" |                 class="mt-3" | ||||||
|                     :key="i" |  | ||||||
|                     cols="12" |  | ||||||
|                     sm="4" |  | ||||||
|             > |             > | ||||||
|                     <h4>{{ t("group") }} {{ i + 1 }}</h4> |                 <strong>{{ t("unassigned") }}:</strong> | ||||||
|                     <draggable |                 <div class="d-flex flex-wrap"> | ||||||
|                         v-model="groupList[i]" |                     <label>{{unassignedStudents.length}}</label> | ||||||
|                         group="students" |                 </div> | ||||||
|                         item-key="username" |             </div> | ||||||
|                         class="group-box" |         </div> | ||||||
|                     > |  | ||||||
|                         <template #item="{ element }"> |  | ||||||
|                             <v-chip>{{ element }}</v-chip> |  | ||||||
|                         </template> |  | ||||||
|                     </draggable> |  | ||||||
| 
 | 
 | ||||||
|  |         <!-- Action Buttons --> | ||||||
|  |         <v-row | ||||||
|  |             justify="center" | ||||||
|  |             class="mb-4" | ||||||
|  |         > | ||||||
|             <v-btn |             <v-btn | ||||||
|                         color="error" |                 color="primary" | ||||||
|                         size="x-small" |                 @click="activeDialog = 'random'" | ||||||
|                         @click="removeGroup(i)" |  | ||||||
|                         class="mt-2" |  | ||||||
|             > |             > | ||||||
|                         {{ t("remove-group") }} |                 {{ t("randomly-create-groups") }} | ||||||
|  |             </v-btn> | ||||||
|  |             <v-btn | ||||||
|  |                 color="secondary" | ||||||
|  |                 class="ml-4" | ||||||
|  |                 @click="activeDialog = 'dragdrop'" | ||||||
|  |             > | ||||||
|  |                 {{ t("drag-and-drop") }} | ||||||
|  |             </v-btn> | ||||||
|  |         </v-row> | ||||||
|  | 
 | ||||||
|  |         <!-- Random Groups Dialog --> | ||||||
|  |         <v-dialog | ||||||
|  |             :model-value="activeDialog === 'random'" | ||||||
|  |             @update:model-value="(val) => (val ? (activeDialog = 'random') : (activeDialog = null))" | ||||||
|  |             max-width="600" | ||||||
|  |         > | ||||||
|  |             <v-card> | ||||||
|  |                 <v-card-title>{{ t("randomly-create-groups") }}</v-card-title> | ||||||
|  |                 <v-card-text> | ||||||
|  |                     <v-row align="center"> | ||||||
|  |                         <v-col cols="6"> | ||||||
|  |                             <v-text-field | ||||||
|  |                                 v-model.number="groupSize" | ||||||
|  |                                 type="number" | ||||||
|  |                                 min="1" | ||||||
|  |                                 :max="allStudents.length" | ||||||
|  |                                 :label="t('group-size-label')" | ||||||
|  |                                 dense | ||||||
|  |                             /> | ||||||
|  |                         </v-col> | ||||||
|  |                         <v-col cols="6"> | ||||||
|  |                             <v-btn | ||||||
|  |                                 color="primary" | ||||||
|  |                                 @click="generateRandomGroups" | ||||||
|  |                                 :disabled="groupSize < 1" | ||||||
|  |                                 block | ||||||
|  |                             > | ||||||
|  |                                 {{ t("generate-groups") }} | ||||||
|                             </v-btn> |                             </v-btn> | ||||||
|                         </v-col> |                         </v-col> | ||||||
|                     </v-row> |                     </v-row> | ||||||
| 
 | 
 | ||||||
|             <v-btn |                     <div class="mt-4"> | ||||||
|                 color="primary" |                         <div class="d-flex justify-space-between align-center mb-2"> | ||||||
|                 class="mt-4" |                             <strong>{{ t("preview") }}</strong> | ||||||
|                 @click="addNewGroup" |                             <span class="text-caption"> {{ randomGroupsPreview.length }} {{ t("groups") }} </span> | ||||||
|  |                         </div> | ||||||
|  | 
 | ||||||
|  |                         <v-expansion-panels> | ||||||
|  |                             <v-expansion-panel | ||||||
|  |                                 v-for="(group, index) in randomGroupsPreview" | ||||||
|  |                                 :key="'random-preview-' + index" | ||||||
|                             > |                             > | ||||||
|                 {{ t("add-group") }} |                                 <v-expansion-panel-title> | ||||||
|             </v-btn> |                                     {{ t("group") }} {{ index + 1 }} ({{ group.length }} {{ t("members") }}) | ||||||
|  |                                 </v-expansion-panel-title> | ||||||
|  |                                 <v-expansion-panel-text> | ||||||
|  |                                     <v-chip | ||||||
|  |                                         v-for="student in group" | ||||||
|  |                                         :key="student.username" | ||||||
|  |                                         class="ma-1" | ||||||
|  |                                     > | ||||||
|  |                                         {{ student.fullName }} | ||||||
|  |                                     </v-chip> | ||||||
|  |                                 </v-expansion-panel-text> | ||||||
|  |                             </v-expansion-panel> | ||||||
|  |                         </v-expansion-panels> | ||||||
|  |                     </div> | ||||||
|                 </v-card-text> |                 </v-card-text> | ||||||
|  | 
 | ||||||
|                 <v-card-actions> |                 <v-card-actions> | ||||||
|  |                     <v-spacer /> | ||||||
|  |                     <v-btn | ||||||
|  |                         text | ||||||
|  |                         @click="activeDialog = null" | ||||||
|  |                         >{{ t("cancel") }}</v-btn | ||||||
|  |                     > | ||||||
|                     <v-btn |                     <v-btn | ||||||
|                         color="success" |                         color="success" | ||||||
|                 @click="saveChanges" |                         @click="saveRandomGroups" | ||||||
|  |                         :disabled="randomGroupsPreview.length === 0" | ||||||
|                     > |                     > | ||||||
|                         {{ t("save") }} |                         {{ t("save") }} | ||||||
|                     </v-btn> |                     </v-btn> | ||||||
|             <v-btn |                 </v-card-actions> | ||||||
|                 @click="$emit('done')" |             </v-card> | ||||||
|                 variant="text" |         </v-dialog> | ||||||
|  | 
 | ||||||
|  |         <!-- Drag and Drop Dialog --> | ||||||
|  |         <v-dialog | ||||||
|  |             :model-value="activeDialog === 'dragdrop'" | ||||||
|  |             @update:model-value="(val) => (val ? (activeDialog = 'dragdrop') : (activeDialog = null))" | ||||||
|  |             max-width="900" | ||||||
|         > |         > | ||||||
|                 {{ t("cancel") }} |             <v-card> | ||||||
|  |                 <v-card-title class="d-flex justify-space-between align-center"> | ||||||
|  |                     <div>{{ t("drag-and-drop") }}</div> | ||||||
|  |                     <v-btn color="primary" small @click="addNewGroup">+</v-btn> | ||||||
|  |                 </v-card-title> | ||||||
|  | 
 | ||||||
|  |                 <v-card-text> | ||||||
|  |                     <v-row> | ||||||
|  |                         <!-- Groups Column --> | ||||||
|  |                         <v-col cols="12" md="8"> | ||||||
|  |                             <div v-if="currentGroups.length === 0" class="text-center py-4"> | ||||||
|  |                                 <v-alert type="info">{{ t("no-groups-yet") }}</v-alert> | ||||||
|  |                             </div> | ||||||
|  | 
 | ||||||
|  |                             <template v-else> | ||||||
|  |                                 <div | ||||||
|  |                                     v-for="(group, groupIndex) in currentGroups" | ||||||
|  |                                     :key="groupIndex" | ||||||
|  |                                     class="mb-4" | ||||||
|  |                                     @dragover.prevent="handleDragOver($event, groupIndex)" | ||||||
|  |                                     @drop="handleDrop($event, groupIndex)" | ||||||
|  |                                 > | ||||||
|  |                                     <div class="d-flex justify-space-between align-center mb-2"> | ||||||
|  |                                         <strong>{{ t("group") }} {{ groupIndex + 1 }}</strong> | ||||||
|  |                                         <v-btn icon small color="error" @click="removeGroup(groupIndex)"> | ||||||
|  |                                             <v-icon>mdi-delete</v-icon> | ||||||
|  |                                         </v-btn> | ||||||
|  |                                     </div> | ||||||
|  | 
 | ||||||
|  |                                     <div class="group-box pa-2"> | ||||||
|  |                                         <div | ||||||
|  |                                             v-for="(student, studentIndex) in group" | ||||||
|  |                                             :key="student.username" | ||||||
|  |                                             class="draggable-item ma-1" | ||||||
|  |                                             draggable="true" | ||||||
|  |                                             @dragstart="handleDragStart(groupIndex, studentIndex)" | ||||||
|  |                                             @dragover.prevent="handleDragOver($event, groupIndex)" | ||||||
|  |                                             @drop="handleDrop($event, groupIndex, studentIndex)" | ||||||
|  |                                         > | ||||||
|  |                                             <v-chip close @click:close="removeStudent(groupIndex, student)"> | ||||||
|  |                                                 {{ student.fullName }} | ||||||
|  |                                             </v-chip> | ||||||
|  |                                         </div> | ||||||
|  |                                     </div> | ||||||
|  |                                 </div> | ||||||
|  |                             </template> | ||||||
|  |                         </v-col> | ||||||
|  | 
 | ||||||
|  |                         <!-- Unassigned Students Column --> | ||||||
|  |                         <v-col | ||||||
|  |                             cols="12" | ||||||
|  |                             md="4" | ||||||
|  |                             @dragover.prevent="handleDragOver($event, -1)" | ||||||
|  |                             @drop="handleDrop($event, -1)" | ||||||
|  |                         > | ||||||
|  |                             <div class="mb-2"> | ||||||
|  |                                 <strong>{{ t("unassigned") }}</strong> | ||||||
|  |                                 <span class="text-caption ml-2">({{ unassignedStudents.length }})</span> | ||||||
|  |                             </div> | ||||||
|  | 
 | ||||||
|  |                             <div class="group-box pa-2"> | ||||||
|  |                                 <div | ||||||
|  |                                     v-for="(student, studentIndex) in unassignedStudents" | ||||||
|  |                                     :key="student.username" | ||||||
|  |                                     class="draggable-item ma-1" | ||||||
|  |                                     draggable="true" | ||||||
|  |                                     @dragstart="handleDragStart(-1, studentIndex)" | ||||||
|  |                                     @dragover.prevent="handleDragOver($event, -1)" | ||||||
|  |                                     @drop="handleDrop($event, -1, studentIndex)" | ||||||
|  |                                 > | ||||||
|  |                                     <v-chip>{{ student.fullName }}</v-chip> | ||||||
|  |                                 </div> | ||||||
|  |                             </div> | ||||||
|  |                         </v-col> | ||||||
|  |                     </v-row> | ||||||
|  |                 </v-card-text> | ||||||
|  | 
 | ||||||
|  |                 <v-card-actions> | ||||||
|  |                     <v-spacer /> | ||||||
|  |                     <v-btn text @click="activeDialog = null">{{ t("cancel") }}</v-btn> | ||||||
|  |                     <v-btn | ||||||
|  |                         color="primary" | ||||||
|  |                         @click="saveDragDrop" | ||||||
|  |                         :disabled="unassignedStudents.length > 0" | ||||||
|  |                     > | ||||||
|  |                         {{ t("save") }} | ||||||
|                     </v-btn> |                     </v-btn> | ||||||
|                 </v-card-actions> |                 </v-card-actions> | ||||||
|             </v-card> |             </v-card> | ||||||
|  |         </v-dialog> | ||||||
|  |     </v-card> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <style scoped> | <style scoped> | ||||||
|     .group-box { |     .group-box { | ||||||
|     min-height: 100px; |         min-height: 150px; | ||||||
|     border: 1px dashed #ccc; |         max-height: 300px; | ||||||
|     padding: 8px; |         overflow-y: auto; | ||||||
|     margin-bottom: 16px; |  | ||||||
|         background-color: #fafafa; |         background-color: #fafafa; | ||||||
|  |         border-radius: 4px; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .v-expansion-panel-text { | ||||||
|  |         max-height: 200px; | ||||||
|  |         overflow-y: auto; | ||||||
|     } |     } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
|  | @ -74,9 +74,13 @@ async function submitFormHandler(): Promise<void> { | ||||||
|         <h1 class="h1">{{ t("new-assignment") }}</h1> |         <h1 class="h1">{{ t("new-assignment") }}</h1> | ||||||
| 
 | 
 | ||||||
|         <v-card class="form-card elevation-2 pa-6"> |         <v-card class="form-card elevation-2 pa-6"> | ||||||
|             <v-form ref="form" class="form-container" validate-on="submit lazy" @submit.prevent="submitFormHandler"> |             <v-form | ||||||
|  |                 ref="form" | ||||||
|  |                 class="form-container" | ||||||
|  |                 validate-on="submit lazy" | ||||||
|  |                 @submit.prevent="submitFormHandler" | ||||||
|  |             > | ||||||
|                 <v-container class="step-container pa-0"> |                 <v-container class="step-container pa-0"> | ||||||
| 
 |  | ||||||
|                     <!-- Titel veld --> |                     <!-- Titel veld --> | ||||||
|                     <v-text-field |                     <v-text-field | ||||||
|                         v-model="assignmentTitle" |                         v-model="assignmentTitle" | ||||||
|  | @ -90,7 +94,10 @@ async function submitFormHandler(): Promise<void> { | ||||||
|                     /> |                     /> | ||||||
| 
 | 
 | ||||||
|                     <!-- Learning Path keuze --> |                     <!-- Learning Path keuze --> | ||||||
|                     <using-query-result :query-result="learningPathsQueryResults" v-slot="{ data }: { data: LearningPath[] }"> |                     <using-query-result | ||||||
|  |                         :query-result="learningPathsQueryResults" | ||||||
|  |                         v-slot="{ data }: { data: LearningPath[] }" | ||||||
|  |                     > | ||||||
|                         <v-combobox |                         <v-combobox | ||||||
|                             v-model="selectedLearningPath" |                             v-model="selectedLearningPath" | ||||||
|                             :items="data" |                             :items="data" | ||||||
|  | @ -111,7 +118,10 @@ async function submitFormHandler(): Promise<void> { | ||||||
|                     </using-query-result> |                     </using-query-result> | ||||||
| 
 | 
 | ||||||
|                     <!-- Klas keuze --> |                     <!-- Klas keuze --> | ||||||
|                     <using-query-result :query-result="classesQueryResults" v-slot="{ data }: { data: ClassesResponse }"> |                     <using-query-result | ||||||
|  |                         :query-result="classesQueryResults" | ||||||
|  |                         v-slot="{ data }: { data: ClassesResponse }" | ||||||
|  |                     > | ||||||
|                         <v-combobox |                         <v-combobox | ||||||
|                             v-model="selectedClass" |                             v-model="selectedClass" | ||||||
|                             :items="data?.classes ?? []" |                             :items="data?.classes ?? []" | ||||||
|  | @ -153,7 +163,6 @@ async function submitFormHandler(): Promise<void> { | ||||||
|                             {{ t("cancel") }} |                             {{ t("cancel") }} | ||||||
|                         </v-btn> |                         </v-btn> | ||||||
|                     </div> |                     </div> | ||||||
| 
 |  | ||||||
|                 </v-container> |                 </v-container> | ||||||
|             </v-form> |             </v-form> | ||||||
|         </v-card> |         </v-card> | ||||||
|  | @ -194,11 +203,9 @@ async function submitFormHandler(): Promise<void> { | ||||||
|             width: 85%; |             width: 85%; | ||||||
|             padding: 1%; |             padding: 1%; | ||||||
|         } |         } | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @media (max-width: 600px) { |     @media (max-width: 600px) { | ||||||
| 
 |  | ||||||
|         h1 { |         h1 { | ||||||
|             font-size: 32px; |             font-size: 32px; | ||||||
|             text-align: center; |             text-align: center; | ||||||
|  | @ -207,7 +214,6 @@ async function submitFormHandler(): Promise<void> { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @media (max-width: 400px) { |     @media (max-width: 400px) { | ||||||
| 
 |  | ||||||
|         h1 { |         h1 { | ||||||
|             font-size: 24px; |             font-size: 24px; | ||||||
|             text-align: center; |             text-align: center; | ||||||
|  | @ -219,7 +225,4 @@ async function submitFormHandler(): Promise<void> { | ||||||
|         border: 2px solid #0e6942; |         border: 2px solid #0e6942; | ||||||
|         border-radius: 12px; |         border-radius: 12px; | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| </style> | </style> | ||||||
| 
 |  | ||||||
|  |  | ||||||
|  | @ -29,7 +29,6 @@ const username = asyncComputed(async () => { | ||||||
|     const assignmentQueryResult = useAssignmentQuery(() => props.classId, props.assignmentId); |     const assignmentQueryResult = useAssignmentQuery(() => props.classId, props.assignmentId); | ||||||
|     learningPath.value = assignmentQueryResult.data.value?.assignment?.learningPath; |     learningPath.value = assignmentQueryResult.data.value?.assignment?.learningPath; | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|     const groupsQueryResult = useGroupsQuery(props.classId, props.assignmentId, true); |     const groupsQueryResult = useGroupsQuery(props.classId, props.assignmentId, true); | ||||||
|     const group = computed(() => { |     const group = computed(() => { | ||||||
|         const groups = groupsQueryResult.data.value?.groups; |         const groups = groupsQueryResult.data.value?.groups; | ||||||
|  | @ -47,7 +46,6 @@ const group = computed(() => { | ||||||
|             .find((group) => group.members?.some((m) => m.username === username.value)); |             .find((group) => group.members?.some((m) => m.username === username.value)); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|     watchEffect(() => { |     watchEffect(() => { | ||||||
|         learningPath.value = assignmentQueryResult.data.value?.assignment?.learningPath; |         learningPath.value = assignmentQueryResult.data.value?.assignment?.learningPath; | ||||||
|         lang.value = assignmentQueryResult.data.value?.assignment?.language as Language; |         lang.value = assignmentQueryResult.data.value?.assignment?.language as Language; | ||||||
|  | @ -66,10 +64,9 @@ const learningPathParams = computed(() => { | ||||||
|     const lpQueryResult = useGetLearningPathQuery( |     const lpQueryResult = useGetLearningPathQuery( | ||||||
|         () => learningPath.value, |         () => learningPath.value, | ||||||
|         () => lang.value, |         () => lang.value, | ||||||
|     () => learningPathParams.value |         () => learningPathParams.value, | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|     const progressColor = computed(() => { |     const progressColor = computed(() => { | ||||||
|         const progress = calculateProgress(lpQueryResult.data.value); |         const progress = calculateProgress(lpQueryResult.data.value); | ||||||
|         if (progress >= 100) return "success"; |         if (progress >= 100) return "success"; | ||||||
|  | @ -77,7 +74,7 @@ const progressColor = computed(() => { | ||||||
|         return "error"; |         return "error"; | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
| const studentQueries = useStudentsByUsernamesQuery(() => group.value?.members as string[] ?? undefined); |     const studentQueries = useStudentsByUsernamesQuery(() => (group.value?.members as string[]) ?? undefined); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|  | @ -100,9 +97,8 @@ const studentQueries = useStudentsByUsernamesQuery(() => group.value?.members as | ||||||
|                         <v-icon>mdi-arrow-left</v-icon> |                         <v-icon>mdi-arrow-left</v-icon> | ||||||
|                     </v-btn> |                     </v-btn> | ||||||
|                 </div> |                 </div> | ||||||
|                 <v-card-title class="text-h4 assignmentTopTitle">{{ |                 <v-card-title class="text-h4 assignmentTopTitle" | ||||||
|                         assignmentResponse.data.assignment.title |                     >{{ assignmentResponse.data.assignment.title }} | ||||||
|                     }} |  | ||||||
|                 </v-card-title> |                 </v-card-title> | ||||||
| 
 | 
 | ||||||
|                 <v-card-subtitle class="subtitle-section"> |                 <v-card-subtitle class="subtitle-section"> | ||||||
|  | @ -112,14 +108,17 @@ const studentQueries = useStudentsByUsernamesQuery(() => group.value?.members as | ||||||
|                     > |                     > | ||||||
|                         <v-btn |                         <v-btn | ||||||
|                             v-if="lpData" |                             v-if="lpData" | ||||||
|                             :to="group ? `/learningPath/${lpData.hruid}/${assignmentResponse.data.assignment?.language}/${lpData.startNode.learningobjectHruid}?forGroup=${0}&assignmentNo=${assignmentId}&classId=${classId}` : undefined" |                             :to=" | ||||||
|  |                                 group | ||||||
|  |                                     ? `/learningPath/${lpData.hruid}/${assignmentResponse.data.assignment?.language}/${lpData.startNode.learningobjectHruid}?forGroup=${0}&assignmentNo=${assignmentId}&classId=${classId}` | ||||||
|  |                                     : undefined | ||||||
|  |                             " | ||||||
|                             :disabled="!group" |                             :disabled="!group" | ||||||
|                             variant="tonal" |                             variant="tonal" | ||||||
|                             color="primary" |                             color="primary" | ||||||
|                         > |                         > | ||||||
|                             {{ t("learning-path") }} |                             {{ t("learning-path") }} | ||||||
|                         </v-btn> |                         </v-btn> | ||||||
| 
 |  | ||||||
|                     </using-query-result> |                     </using-query-result> | ||||||
|                 </v-card-subtitle> |                 </v-card-subtitle> | ||||||
| 
 | 
 | ||||||
|  | @ -163,12 +162,14 @@ const studentQueries = useStudentsByUsernamesQuery(() => group.value?.members as | ||||||
|                     </div> |                     </div> | ||||||
| 
 | 
 | ||||||
|                     <div v-else> |                     <div v-else> | ||||||
|                         <v-alert type="info" variant="text"> |                         <v-alert | ||||||
|  |                             type="info" | ||||||
|  |                             variant="text" | ||||||
|  |                         > | ||||||
|                             {{ t("not-in-group-message") }} |                             {{ t("not-in-group-message") }} | ||||||
|                         </v-alert> |                         </v-alert> | ||||||
|                     </div> |                     </div> | ||||||
|                 </v-card-text> |                 </v-card-text> | ||||||
| 
 |  | ||||||
|             </v-card> |             </v-card> | ||||||
|         </using-query-result> |         </using-query-result> | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|  | @ -4,7 +4,7 @@ import {useI18n} from "vue-i18n"; | ||||||
|     import { |     import { | ||||||
|         useAssignmentQuery, |         useAssignmentQuery, | ||||||
|         useDeleteAssignmentMutation, |         useDeleteAssignmentMutation, | ||||||
|     useUpdateAssignmentMutation |         useUpdateAssignmentMutation, | ||||||
|     } from "@/queries/assignments.ts"; |     } from "@/queries/assignments.ts"; | ||||||
|     import UsingQueryResult from "@/components/UsingQueryResult.vue"; |     import UsingQueryResult from "@/components/UsingQueryResult.vue"; | ||||||
|     import { useGroupsQuery } from "@/queries/groups.ts"; |     import { useGroupsQuery } from "@/queries/groups.ts"; | ||||||
|  | @ -14,8 +14,8 @@ import type {AssignmentResponse} from "@/controllers/assignments.ts"; | ||||||
|     import type { GroupDTO, GroupDTOId } from "@dwengo-1/common/interfaces/group"; |     import type { GroupDTO, GroupDTOId } from "@dwengo-1/common/interfaces/group"; | ||||||
|     import type { LearningPath } from "@/data-objects/learning-paths/learning-path"; |     import type { LearningPath } from "@/data-objects/learning-paths/learning-path"; | ||||||
|     import { descriptionRules, learningPathRules } from "@/utils/assignment-rules.ts"; |     import { descriptionRules, learningPathRules } from "@/utils/assignment-rules.ts"; | ||||||
| import GroupSubmissionStatus from "@/components/GroupSubmissionStatus.vue" |     import GroupSubmissionStatus from "@/components/GroupSubmissionStatus.vue"; | ||||||
| import GroupProgressRow from "@/components/GroupProgressRow.vue" |     import GroupProgressRow from "@/components/GroupProgressRow.vue"; | ||||||
|     import type { AssignmentDTO } from "@dwengo-1/common/dist/interfaces/assignment.ts"; |     import type { AssignmentDTO } from "@dwengo-1/common/dist/interfaces/assignment.ts"; | ||||||
|     import GroupSelector from "@/components/assignments/GroupSelector.vue"; |     import GroupSelector from "@/components/assignments/GroupSelector.vue"; | ||||||
| 
 | 
 | ||||||
|  | @ -37,12 +37,10 @@ const groups = ref<GroupDTO[] | GroupDTOId[]>([]); | ||||||
|     const learningPath = ref(); |     const learningPath = ref(); | ||||||
|     const form = ref(); |     const form = ref(); | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|     const editingLearningPath = ref(learningPath); |     const editingLearningPath = ref(learningPath); | ||||||
|     const description = ref(""); |     const description = ref(""); | ||||||
|     const editGroups = ref(false); |     const editGroups = ref(false); | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|     const assignmentQueryResult = useAssignmentQuery(() => props.classId, props.assignmentId); |     const assignmentQueryResult = useAssignmentQuery(() => props.classId, props.assignmentId); | ||||||
|     // Get learning path object |     // Get learning path object | ||||||
|     const lpQueryResult = useGetLearningPathQuery( |     const lpQueryResult = useGetLearningPathQuery( | ||||||
|  | @ -140,11 +138,9 @@ async function saveChanges(): Promise<void> { | ||||||
|         mutate({ |         mutate({ | ||||||
|             cid: assignmentQueryResult.data.value?.assignment.within, |             cid: assignmentQueryResult.data.value?.assignment.within, | ||||||
|             an: assignmentQueryResult.data.value?.assignment.id, |             an: assignmentQueryResult.data.value?.assignment.id, | ||||||
|         data: assignmentDTO |             data: assignmentDTO, | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|  | @ -167,7 +163,11 @@ async function saveChanges(): Promise<void> { | ||||||
|                         md="6" |                         md="6" | ||||||
|                         class="responsive-col" |                         class="responsive-col" | ||||||
|                     > |                     > | ||||||
|                         <v-form ref="form" validate-on="submit lazy" @submit.prevent="saveChanges"> |                         <v-form | ||||||
|  |                             ref="form" | ||||||
|  |                             validate-on="submit lazy" | ||||||
|  |                             @submit.prevent="saveChanges" | ||||||
|  |                         > | ||||||
|                             <v-card |                             <v-card | ||||||
|                                 v-if="assignmentResponse" |                                 v-if="assignmentResponse" | ||||||
|                                 class="assignment-card" |                                 class="assignment-card" | ||||||
|  | @ -201,10 +201,14 @@ async function saveChanges(): Promise<void> { | ||||||
|                                                 v-else |                                                 v-else | ||||||
|                                                 variant="text" |                                                 variant="text" | ||||||
|                                                 class="top-right-btn" |                                                 class="top-right-btn" | ||||||
|                                                 @click="() => {isEditing = false; editingLearningPath=learningPath}" |                                                 @click=" | ||||||
|  |                                                     () => { | ||||||
|  |                                                         isEditing = false; | ||||||
|  |                                                         editingLearningPath = learningPath; | ||||||
|  |                                                     } | ||||||
|  |                                                 " | ||||||
|                                                 >{{ t("cancel") }} |                                                 >{{ t("cancel") }} | ||||||
|                                             </v-btn |                                             </v-btn> | ||||||
|                                             > |  | ||||||
| 
 | 
 | ||||||
|                                             <v-btn |                                             <v-btn | ||||||
|                                                 v-if="!isEditing" |                                                 v-if="!isEditing" | ||||||
|  | @ -233,9 +237,8 @@ async function saveChanges(): Promise<void> { | ||||||
|                                     </div> |                                     </div> | ||||||
|                                 </div> |                                 </div> | ||||||
| 
 | 
 | ||||||
|                                 <v-card-title class="text-h4 assignmentTopTitle">{{ |                                 <v-card-title class="text-h4 assignmentTopTitle" | ||||||
|                                         assignmentResponse.data.assignment.title |                                     >{{ assignmentResponse.data.assignment.title }} | ||||||
|                                     }} |  | ||||||
|                                 </v-card-title> |                                 </v-card-title> | ||||||
|                                 <v-card-subtitle |                                 <v-card-subtitle | ||||||
|                                     v-if="!isEditing" |                                     v-if="!isEditing" | ||||||
|  | @ -328,15 +331,13 @@ async function saveChanges(): Promise<void> { | ||||||
|                                         color="primary" |                                         color="primary" | ||||||
|                                         @click="dialog = false" |                                         @click="dialog = false" | ||||||
|                                         >Close |                                         >Close | ||||||
|                                     </v-btn |                                     </v-btn> | ||||||
|                                     > |  | ||||||
|                                 </v-card-actions> |                                 </v-card-actions> | ||||||
|                             </v-card> |                             </v-card> | ||||||
|                         </v-dialog> |                         </v-dialog> | ||||||
|                     </v-col> |                     </v-col> | ||||||
| 
 | 
 | ||||||
|                     <!-- The second column of the screen --> |                     <!-- The second column of the screen --> | ||||||
|                     <template v-if="!editGroups"> |  | ||||||
|                     <v-col |                     <v-col | ||||||
|                         cols="12" |                         cols="12" | ||||||
|                         sm="6" |                         sm="6" | ||||||
|  | @ -356,7 +357,6 @@ async function saveChanges(): Promise<void> { | ||||||
|                                         > |                                         > | ||||||
|                                             <v-icon>mdi-pencil</v-icon> |                                             <v-icon>mdi-pencil</v-icon> | ||||||
|                                         </v-btn> |                                         </v-btn> | ||||||
| 
 |  | ||||||
|                                     </th> |                                     </th> | ||||||
|                                 </tr> |                                 </tr> | ||||||
|                             </thead> |                             </thead> | ||||||
|  | @ -403,21 +403,26 @@ async function saveChanges(): Promise<void> { | ||||||
|                                         > |                                         > | ||||||
|                                             <v-icon color="red">mdi-delete</v-icon> |                                             <v-icon color="red">mdi-delete</v-icon> | ||||||
|                                         </v-btn> |                                         </v-btn> | ||||||
| 
 |  | ||||||
|                                     </td> |                                     </td> | ||||||
|                                 </tr> |                                 </tr> | ||||||
|                             </tbody> |                             </tbody> | ||||||
|                         </v-table> |                         </v-table> | ||||||
|                     </v-col> |                     </v-col> | ||||||
|                     </template> |                 </v-row> | ||||||
|                     <template v-else> |                 <v-dialog | ||||||
|  |                     v-model="editGroups" | ||||||
|  |                     max-width="800" | ||||||
|  |                     persistent | ||||||
|  |                 > | ||||||
|  |                     <v-card-text> | ||||||
|                         <GroupSelector |                         <GroupSelector | ||||||
|                             :groups="allGroups" |                             :groups="allGroups" | ||||||
|                             :class-id="classId" |                             :class-id="props.classId" | ||||||
|                             @groupsUpdated="handleUpdatedGroups" |                             :assignment-id="props.assignmentId" | ||||||
|  |                             @close="editGroups = false" | ||||||
|                         /> |                         /> | ||||||
|                     </template> |                     </v-card-text> | ||||||
|                 </v-row> |                 </v-dialog> | ||||||
|             </v-container> |             </v-container> | ||||||
|         </using-query-result> |         </using-query-result> | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|  | @ -34,9 +34,13 @@ onMounted(async () => { | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     const isTeacher = computed(() => role.value === "teacher"); |     const isTeacher = computed(() => role.value === "teacher"); | ||||||
| const classesQueryResult = isTeacher.value ? useTeacherClassesQuery(username, true) : useStudentClassesQuery(username, true); |     const classesQueryResult = isTeacher.value | ||||||
|  |         ? useTeacherClassesQuery(username, true) | ||||||
|  |         : useStudentClassesQuery(username, true); | ||||||
| 
 | 
 | ||||||
| const assignmentsQueryResult = isTeacher.value ? useTeacherAssignmentsQuery(username, true) : useStudentAssignmentsQuery(username, true); |     const assignmentsQueryResult = isTeacher.value | ||||||
|  |         ? useTeacherAssignmentsQuery(username, true) | ||||||
|  |         : useStudentAssignmentsQuery(username, true); | ||||||
| 
 | 
 | ||||||
|     const allAssignments = asyncComputed( |     const allAssignments = asyncComputed( | ||||||
|         async () => { |         async () => { | ||||||
|  | @ -134,8 +138,6 @@ onMounted(async () => { | ||||||
|         const user = await auth.loadUser(); |         const user = await auth.loadUser(); | ||||||
|         username.value = user?.profile?.preferred_username ?? ""; |         username.value = user?.profile?.preferred_username ?? ""; | ||||||
|     }); |     }); | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|  | @ -151,9 +153,7 @@ onMounted(async () => { | ||||||
|             {{ t("new-assignment") }} |             {{ t("new-assignment") }} | ||||||
|         </v-btn> |         </v-btn> | ||||||
| 
 | 
 | ||||||
|         <using-query-result |         <using-query-result :query-result="assignmentsQueryResult"> | ||||||
|             :query-result="assignmentsQueryResult" |  | ||||||
|         > |  | ||||||
|             <v-container> |             <v-container> | ||||||
|                 <v-row> |                 <v-row> | ||||||
|                     <v-col |                     <v-col | ||||||
|  | @ -238,7 +238,8 @@ onMounted(async () => { | ||||||
|         border-radius: 16px; |         border-radius: 16px; | ||||||
|         box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); |         box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); | ||||||
|         background-color: white; |         background-color: white; | ||||||
|     transition: transform 0.2s, |         transition: | ||||||
|  |             transform 0.2s, | ||||||
|             box-shadow 0.2s; |             box-shadow 0.2s; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										3606
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										3606
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
		Reference in a new issue
	
	 Joyelle Ndagijimana
						Joyelle Ndagijimana