feat: drag en drop werkt op mobiel
This commit is contained in:
		
							parent
							
								
									45563b68ea
								
							
						
					
					
						commit
						862e72ef4a
					
				
					 4 changed files with 489 additions and 277 deletions
				
			
		|  | @ -21,6 +21,7 @@ | |||
|         "@tanstack/vue-query": "^5.69.0", | ||||
|         "@vueuse/core": "^13.1.0", | ||||
|         "axios": "^1.8.2", | ||||
|         "interactjs": "^1.10.27", | ||||
|         "oidc-client-ts": "^3.1.0", | ||||
|         "rollup": "^4.40.0", | ||||
|         "uuid": "^11.1.0", | ||||
|  |  | |||
|  | @ -17,10 +17,10 @@ | |||
| 
 | ||||
| const {data: studentsData} = useClassStudentsQuery(() => props.classId, true); | ||||
| 
 | ||||
|     // Dialog states | ||||
| // Dialog states for group editing | ||||
| const activeDialog = ref<"random" | "dragdrop" | null>(null); | ||||
| 
 | ||||
|     // Drag state | ||||
| // Drag state for the drag and drop | ||||
| const draggedItem = ref<{ groupIndex: number, studentIndex: number } | null>(null); | ||||
| 
 | ||||
| const currentGroups = ref<StudentItem[][]>([]); | ||||
|  | @ -56,20 +56,17 @@ | |||
|             ); | ||||
|             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() { | ||||
| /** Random groups functions */ | ||||
| function generateRandomGroups(): void { | ||||
|     if (groupSize.value < 1) return; | ||||
| 
 | ||||
|     // Shuffle students | ||||
|  | @ -92,7 +89,7 @@ | |||
|     randomGroupsPreview.value = newGroups; | ||||
| } | ||||
| 
 | ||||
|     function saveRandomGroups() { | ||||
| function saveRandomGroups(): void { | ||||
|     if (randomGroupsPreview.value.length === 0) { | ||||
|         alert(t("please-generate-groups-first")); | ||||
|         return; | ||||
|  | @ -107,7 +104,6 @@ | |||
|     emit("close"); | ||||
| } | ||||
| 
 | ||||
|     // Drag and drop functions | ||||
| function addNewGroup() { | ||||
|     currentGroups.value.push([]); | ||||
| } | ||||
|  | @ -118,42 +114,211 @@ | |||
|     currentGroups.value.splice(index, 1); | ||||
| } | ||||
| 
 | ||||
|     // Native Drag & Drop Handlers | ||||
|     function handleDragStart(groupIndex: number, studentIndex: number) { | ||||
| /** Drag and drop functions */ | ||||
| 
 | ||||
| // Touch state interface | ||||
| interface TouchState { | ||||
|     isDragging: boolean; | ||||
|     startX: number; | ||||
|     startY: number; | ||||
|     currentGroupIndex: number; | ||||
|     currentStudentIndex: number; | ||||
|     element: HTMLElement | null; | ||||
|     clone: HTMLElement | null; | ||||
|     originalRect: DOMRect | null; | ||||
|     hasMoved: boolean; | ||||
| } | ||||
| 
 | ||||
| const touchState = ref<TouchState>({ | ||||
|     isDragging: false, | ||||
|     startX: 0, | ||||
|     startY: 0, | ||||
|     currentGroupIndex: -1, | ||||
|     currentStudentIndex: -1, | ||||
|     element: null, | ||||
|     clone: null, | ||||
|     originalRect: null, | ||||
|     hasMoved: false | ||||
| }); | ||||
| 
 | ||||
| function handleTouchStart(event: TouchEvent, groupIndex: number, studentIndex: number): void { | ||||
|     if (event.touches.length > 1) return; | ||||
| 
 | ||||
|     const touch = event.touches[0]; | ||||
|     const target = event.target as HTMLElement; | ||||
|     // Target the chip directly instead of the draggable container | ||||
|     const chip = target.closest('.v-chip') as HTMLElement; | ||||
| 
 | ||||
|     if (!chip) return; | ||||
| 
 | ||||
|     // Get the chip's position relative to the viewport | ||||
|     const rect = chip.getBoundingClientRect(); | ||||
| 
 | ||||
|     touchState.value = { | ||||
|         isDragging: true, | ||||
|         startX: touch.clientX, | ||||
|         startY: touch.clientY, | ||||
|         currentGroupIndex: groupIndex, | ||||
|         currentStudentIndex: studentIndex, | ||||
|         element: chip, | ||||
|         clone: null, | ||||
|         originalRect: rect, | ||||
|         hasMoved: false | ||||
|     }; | ||||
| 
 | ||||
|     // Clone only the chip | ||||
|     const clone = chip.cloneNode(true) as HTMLElement; | ||||
|     clone.classList.add('drag-clone'); | ||||
|     clone.style.position = 'fixed'; | ||||
|     clone.style.zIndex = '10000'; | ||||
|     clone.style.opacity = '0.9'; | ||||
|     clone.style.pointerEvents = 'none'; | ||||
|     clone.style.width = `${rect.width}px`; | ||||
|     clone.style.height = `${rect.height}px`; | ||||
|     clone.style.left = `${rect.left}px`; | ||||
|     clone.style.top = `${rect.top}px`; | ||||
|     clone.style.transform = 'scale(1.05)'; | ||||
|     clone.style.boxShadow = '0 4px 8px rgba(0,0,0,0.3)'; | ||||
|     clone.style.transition = 'transform 0.1s'; | ||||
| 
 | ||||
|     // Ensure the clone has the same chip styling | ||||
|     clone.style.backgroundColor = getComputedStyle(chip).backgroundColor; | ||||
|     clone.style.color = getComputedStyle(chip).color; | ||||
|     clone.style.borderRadius = getComputedStyle(chip).borderRadius; | ||||
|     clone.style.padding = getComputedStyle(chip).padding; | ||||
|     clone.style.margin = '0'; // Remove any margin | ||||
| 
 | ||||
|     document.body.appendChild(clone); | ||||
|     touchState.value.clone = clone; | ||||
|     chip.style.visibility = 'hidden'; | ||||
| 
 | ||||
|     event.preventDefault(); | ||||
|     event.stopPropagation(); | ||||
| } | ||||
| 
 | ||||
| function handleTouchMove(event: TouchEvent): void { | ||||
|     if (!touchState.value.isDragging || !touchState.value.clone || event.touches.length > 1) return; | ||||
| 
 | ||||
|     const touch = event.touches[0]; | ||||
|     const clone = touchState.value.clone; | ||||
| 
 | ||||
|     const dx = Math.abs(touch.clientX - touchState.value.startX); | ||||
|     const dy = Math.abs(touch.clientY - touchState.value.startY); | ||||
| 
 | ||||
|     if (dx > 5 || dy > 5) { | ||||
|         touchState.value.hasMoved = true; | ||||
|     } | ||||
| 
 | ||||
|     clone.style.left = `${touch.clientX - clone.offsetWidth / 2}px`; | ||||
|     clone.style.top = `${touch.clientY - clone.offsetHeight / 2}px`; | ||||
| 
 | ||||
|     document.querySelectorAll('.group-box').forEach(el => { | ||||
|         el.classList.remove('highlight'); | ||||
|     }); | ||||
| 
 | ||||
|     const elements = document.elementsFromPoint(touch.clientX, touch.clientY); | ||||
|     const dropTarget = elements.find(el => el.classList.contains('group-box')); | ||||
| 
 | ||||
|     if (dropTarget) { | ||||
|         dropTarget.classList.add('highlight'); | ||||
|     } | ||||
| 
 | ||||
|     event.preventDefault(); | ||||
|     event.stopPropagation(); | ||||
| } | ||||
| 
 | ||||
| function handleTouchEnd(event: TouchEvent): void { | ||||
|     if (!touchState.value.isDragging) return; | ||||
| 
 | ||||
|     const { | ||||
|         currentGroupIndex, | ||||
|         currentStudentIndex, | ||||
|         clone, | ||||
|         element, | ||||
|         hasMoved | ||||
|     } = touchState.value; | ||||
| 
 | ||||
|     document.querySelectorAll('.group-box').forEach(el => { | ||||
|         el.classList.remove('highlight'); | ||||
|     }); | ||||
| 
 | ||||
|     if (clone?.parentNode) { | ||||
|         clone.parentNode.removeChild(clone); | ||||
|     } | ||||
| 
 | ||||
|     if (element) { | ||||
|         element.style.visibility = 'visible'; | ||||
|     } | ||||
| 
 | ||||
|     if (hasMoved && event.changedTouches.length > 0) { | ||||
|         const touch = event.changedTouches[0]; | ||||
|         const elements = document.elementsFromPoint(touch.clientX, touch.clientY); | ||||
|         const dropTarget = elements.find(el => el.classList.contains('group-box')); | ||||
| 
 | ||||
|         if (dropTarget) { | ||||
|             const groupBoxes = document.querySelectorAll('.group-box'); | ||||
|             const targetGroupIndex = Array.from(groupBoxes).indexOf(dropTarget); | ||||
| 
 | ||||
|             if (targetGroupIndex !== currentGroupIndex) { | ||||
|                 const sourceArray = currentGroupIndex === -1 | ||||
|                     ? unassignedStudents.value | ||||
|                     : currentGroups.value[currentGroupIndex]; | ||||
|                 const targetArray = targetGroupIndex === -1 | ||||
|                     ? unassignedStudents.value | ||||
|                     : currentGroups.value[targetGroupIndex]; | ||||
| 
 | ||||
|                 if (sourceArray && targetArray) { | ||||
|                     const [movedStudent] = sourceArray.splice(currentStudentIndex, 1); | ||||
|                     targetArray.push(movedStudent); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     touchState.value = { | ||||
|         isDragging: false, | ||||
|         startX: 0, | ||||
|         startY: 0, | ||||
|         currentGroupIndex: -1, | ||||
|         currentStudentIndex: -1, | ||||
|         element: null, | ||||
|         clone: null, | ||||
|         originalRect: null, | ||||
|         hasMoved: false | ||||
|     }; | ||||
| 
 | ||||
|     event.preventDefault(); | ||||
|     event.stopPropagation(); | ||||
| } | ||||
| 
 | ||||
| function handleDragStart(event: DragEvent, groupIndex: number, studentIndex: number): void { | ||||
|     draggedItem.value = {groupIndex, studentIndex}; | ||||
|     if (event.dataTransfer) { | ||||
|         event.dataTransfer.effectAllowed = 'move'; | ||||
|         event.dataTransfer.setData('text/plain', ''); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|     function handleDragOver(e: DragEvent, _: number) { | ||||
| function handleDragOver(e: DragEvent, _: number): void { | ||||
|     e.preventDefault(); | ||||
|         e.dataTransfer!.dropEffect = "move"; | ||||
|     if (e.dataTransfer) { | ||||
|         e.dataTransfer.dropEffect = "move"; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|     function handleDrop(e: DragEvent, targetGroupIndex: number, targetStudentIndex?: number) { | ||||
| function handleDrop(e: DragEvent, targetGroupIndex: number, targetStudentIndex?: number): void { | ||||
|     e.preventDefault(); | ||||
|     if (!draggedItem.value) return; | ||||
| 
 | ||||
|     const {groupIndex: sourceGroupIndex, studentIndex: sourceStudentIndex} = draggedItem.value; | ||||
|         const isSameGroup = sourceGroupIndex === targetGroupIndex; | ||||
|     const sourceArray = sourceGroupIndex === -1 | ||||
|         ? unassignedStudents.value | ||||
|         : currentGroups.value[sourceGroupIndex]; | ||||
|     const targetArray = targetGroupIndex === -1 | ||||
|         ? unassignedStudents.value | ||||
|         : currentGroups.value[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 { | ||||
|  | @ -163,8 +328,7 @@ | |||
|     draggedItem.value = null; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
|     function saveDragDrop() { | ||||
| function saveDragDrop(): void { | ||||
|     if (unassignedStudents.value.length > 0) { | ||||
|         alert(t("please-assign-all-students")); | ||||
|         return; | ||||
|  | @ -179,12 +343,11 @@ | |||
|     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) { | ||||
| function removeStudent(groupIndex: number, student: StudentItem): void { | ||||
|     const group = currentGroups.value[groupIndex]; | ||||
|     currentGroups.value[groupIndex] = group.filter((s) => s.username !== student.username); | ||||
|     unassignedStudents.value.push(student); | ||||
|  | @ -193,23 +356,16 @@ | |||
| 
 | ||||
| <template> | ||||
|     <v-card class="pa-4"> | ||||
|         <!-- Current Groups Preview --> | ||||
|         <div | ||||
|             v-if="showGroupsPreview" | ||||
|             class="mb-6" | ||||
|         > | ||||
|         <!-- Current groups and unassigned students Preview --> | ||||
|         <div v-if="showGroupsPreview" class="mb-6"> | ||||
|             <h3 class="mb-2">{{ t("current-groups") }}</h3> | ||||
|             <div | ||||
|             > | ||||
|             <div> | ||||
|                 <div class="d-flex flex-wrap"> | ||||
|                     <label>{{ currentGroups.length }}</label> | ||||
|                 </div> | ||||
|             </div> | ||||
| 
 | ||||
|             <div | ||||
|                 v-if="unassignedStudents.length > 0" | ||||
|                 class="mt-3" | ||||
|             > | ||||
|             <div v-if="unassignedStudents.length > 0" class="mt-3"> | ||||
|                 <strong>{{ t("unassigned-students") }}:</strong> | ||||
|                 <div class="d-flex flex-wrap"> | ||||
|                     <label>{{ unassignedStudents.length }}</label> | ||||
|  | @ -217,27 +373,16 @@ | |||
|             </div> | ||||
|         </div> | ||||
| 
 | ||||
|         <!-- Action Buttons --> | ||||
|         <v-row | ||||
|             justify="center" | ||||
|             class="mb-4" | ||||
|         > | ||||
|             <v-btn | ||||
|                 color="primary" | ||||
|                 @click="activeDialog = 'random'" | ||||
|             > | ||||
|         <v-row justify="center" class="mb-4"> | ||||
|             <v-btn color="primary" @click="activeDialog = 'random'"> | ||||
|                 {{ t("randomly-create-groups") }} | ||||
|             </v-btn> | ||||
|             <v-btn | ||||
|                 color="secondary" | ||||
|                 class="ml-4" | ||||
|                 @click="activeDialog = 'dragdrop'" | ||||
|             > | ||||
|             <v-btn color="secondary" class="ml-4" @click="activeDialog = 'dragdrop'"> | ||||
|                 {{ t("drag-and-drop") }} | ||||
|             </v-btn> | ||||
|         </v-row> | ||||
| 
 | ||||
|         <!-- Random Groups Dialog --> | ||||
|         <!-- Random Groups selection Dialog --> | ||||
|         <v-dialog | ||||
|             :model-value="activeDialog === 'random'" | ||||
|             @update:model-value="(val) => (val ? (activeDialog = 'random') : (activeDialog = null))" | ||||
|  | @ -299,11 +444,7 @@ | |||
| 
 | ||||
|                 <v-card-actions> | ||||
|                     <v-spacer/> | ||||
|                     <v-btn | ||||
|                         text | ||||
|                         @click="activeDialog = null" | ||||
|                         >{{ t("cancel") }}</v-btn | ||||
|                     > | ||||
|                     <v-btn text @click="activeDialog = null">{{ t("cancel") }}</v-btn> | ||||
|                     <v-btn | ||||
|                         color="success" | ||||
|                         @click="saveRandomGroups" | ||||
|  | @ -356,7 +497,10 @@ | |||
|                                             :key="student.username" | ||||
|                                             class="draggable-item ma-1" | ||||
|                                             draggable="true" | ||||
|                                             @dragstart="handleDragStart(groupIndex, studentIndex)" | ||||
|                                             @touchstart="handleTouchStart($event, groupIndex, studentIndex)" | ||||
|                                             @touchmove="handleTouchMove($event)" | ||||
|                                             @touchend="handleTouchEnd($event)" | ||||
|                                             @dragstart="handleDragStart($event, groupIndex, studentIndex)" | ||||
|                                             @dragover.prevent="handleDragOver($event, groupIndex)" | ||||
|                                             @drop="handleDrop($event, groupIndex, studentIndex)" | ||||
|                                         > | ||||
|  | @ -387,7 +531,10 @@ | |||
|                                     :key="student.username" | ||||
|                                     class="draggable-item ma-1" | ||||
|                                     draggable="true" | ||||
|                                     @dragstart="handleDragStart(-1, studentIndex)" | ||||
|                                     @touchstart="handleTouchStart($event, -1, studentIndex)" | ||||
|                                     @touchmove="handleTouchMove($event)" | ||||
|                                     @touchend="handleTouchEnd($event)" | ||||
|                                     @dragstart="handleDragStart($event, -1, studentIndex)" | ||||
|                                     @dragover.prevent="handleDragOver($event, -1)" | ||||
|                                     @drop="handleDrop($event, -1, studentIndex)" | ||||
|                                 > | ||||
|  | @ -416,15 +563,44 @@ | |||
| 
 | ||||
| <style scoped> | ||||
| .group-box { | ||||
|         min-height: 150px; | ||||
|         max-height: 300px; | ||||
|     min-height: 100px; | ||||
|     max-height: 200px; | ||||
|     overflow-y: auto; | ||||
|     background-color: #fafafa; | ||||
|     border-radius: 4px; | ||||
|     transition: all 0.2s; | ||||
| } | ||||
| 
 | ||||
| .group-box.highlight { | ||||
|     background-color: #e3f2fd; | ||||
|     border: 2px dashed #2196f3; | ||||
| } | ||||
| 
 | ||||
| .v-expansion-panel-text { | ||||
|     max-height: 200px; | ||||
|     overflow-y: auto; | ||||
| } | ||||
| 
 | ||||
| .drag-clone { | ||||
|     z-index: 10000; | ||||
|     transform: scale(1.05); | ||||
|     box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); | ||||
|     transition: transform 0.1s; | ||||
|     will-change: transform; | ||||
|     pointer-events: none; | ||||
|     display: inline-flex; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
|     border-radius: 16px; | ||||
|     background-color: inherit; | ||||
| } | ||||
| 
 | ||||
| .draggable-item { | ||||
|     display: inline-block; | ||||
| } | ||||
| 
 | ||||
| .draggable-item .v-chip[style*="hidden"] { | ||||
|     visibility: hidden; | ||||
|     display: inline-block; | ||||
| } | ||||
| </style> | ||||
|  |  | |||
|  | @ -344,6 +344,7 @@ async function saveChanges(): Promise<void> { | |||
|                         md="6" | ||||
|                         class="responsive-col" | ||||
|                     > | ||||
|                         <div class="table-container"> | ||||
|                             <v-table class="table"> | ||||
|                                 <thead> | ||||
|                                 <tr> | ||||
|  | @ -407,6 +408,7 @@ async function saveChanges(): Promise<void> { | |||
|                                 </tr> | ||||
|                                 </tbody> | ||||
|                             </v-table> | ||||
|                         </div> | ||||
|                     </v-col> | ||||
|                 </v-row> | ||||
|                 <v-dialog | ||||
|  | @ -510,6 +512,17 @@ main { | |||
|     margin-left: 30px; | ||||
| } | ||||
| 
 | ||||
| .table-container { | ||||
|     width: 100%; | ||||
|     overflow-x: visible; | ||||
| } | ||||
| 
 | ||||
| .table { | ||||
|     width: 100%; | ||||
|     min-width: auto; | ||||
|     table-layout: auto; | ||||
| } | ||||
| 
 | ||||
| @media screen and (max-width: 850px) { | ||||
|     h1 { | ||||
|         text-align: center; | ||||
|  | @ -540,6 +553,12 @@ main { | |||
| 
 | ||||
|     .table { | ||||
|         width: 100%; | ||||
|         display: block; | ||||
|         overflow-x: auto; | ||||
|     } | ||||
| 
 | ||||
|     .table-container { | ||||
|         overflow-x: auto; | ||||
|     } | ||||
| 
 | ||||
|     .responsive-col { | ||||
|  |  | |||
							
								
								
									
										16
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										16
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							|  | @ -101,6 +101,7 @@ | |||
|                 "@tanstack/vue-query": "^5.69.0", | ||||
|                 "@vueuse/core": "^13.1.0", | ||||
|                 "axios": "^1.8.2", | ||||
|                 "interactjs": "^1.10.27", | ||||
|                 "oidc-client-ts": "^3.1.0", | ||||
|                 "rollup": "^4.40.0", | ||||
|                 "uuid": "^11.1.0", | ||||
|  | @ -1596,6 +1597,12 @@ | |||
|                 "url": "https://github.com/sponsors/nzakas" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@interactjs/types": { | ||||
|             "version": "1.10.27", | ||||
|             "resolved": "https://registry.npmjs.org/@interactjs/types/-/types-1.10.27.tgz", | ||||
|             "integrity": "sha512-BUdv0cvs4H5ODuwft2Xp4eL8Vmi3LcihK42z0Ft/FbVJZoRioBsxH+LlsBdK4tAie7PqlKGy+1oyOncu1nQ6eA==", | ||||
|             "license": "MIT" | ||||
|         }, | ||||
|         "node_modules/@intlify/core-base": { | ||||
|             "version": "11.1.3", | ||||
|             "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.1.3.tgz", | ||||
|  | @ -6830,6 +6837,15 @@ | |||
|             "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", | ||||
|             "license": "ISC" | ||||
|         }, | ||||
|         "node_modules/interactjs": { | ||||
|             "version": "1.10.27", | ||||
|             "resolved": "https://registry.npmjs.org/interactjs/-/interactjs-1.10.27.tgz", | ||||
|             "integrity": "sha512-y/8RcCftGAF24gSp76X2JS3XpHiUvDQyhF8i7ujemBz77hwiHDuJzftHx7thY8cxGogwGiPJ+o97kWB6eAXnsA==", | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@interactjs/types": "1.10.27" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/interpret": { | ||||
|             "version": "2.2.0", | ||||
|             "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", | ||||
|  |  | |||
		Reference in a new issue
	
	 Joyelle Ndagijimana
						Joyelle Ndagijimana