feat(frontend): Zoekfunctie voor leerpaden geïmplementeerd
This commit is contained in:
		
							parent
							
								
									a9643838b7
								
							
						
					
					
						commit
						f9e2166504
					
				
					 9 changed files with 186 additions and 14 deletions
				
			
		
							
								
								
									
										36
									
								
								frontend/src/components/LearningPathSearchField.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								frontend/src/components/LearningPathSearchField.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,36 @@ | |||
| <script setup lang="ts"> | ||||
|     import {useI18n} from "vue-i18n"; | ||||
|     import {useRoute, useRouter} from "vue-router"; | ||||
|     import {computed, ref} from "vue"; | ||||
|     const route = useRoute(); | ||||
|     const router = useRouter(); | ||||
|     const { t } = useI18n(); | ||||
| 
 | ||||
|     const SEARCH_PATH = "/learningPath/search"; | ||||
| 
 | ||||
|     const query = computed({ | ||||
|         get: () => route.query.query as string | null, | ||||
|         set: (newValue) => router.push({path: SEARCH_PATH, query: {query: newValue}}) | ||||
|     }); | ||||
| 
 | ||||
|     const queryInput = ref(query.value); | ||||
| 
 | ||||
|     function search() { | ||||
|         query.value = queryInput.value; | ||||
|     } | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|     <v-text-field | ||||
|         class="search-field" | ||||
|         :label="t('search')" | ||||
|         append-inner-icon="mdi-magnify" | ||||
|         v-model="queryInput" | ||||
|         @keyup.enter="search()" | ||||
|         @click:append-inner="search()" | ||||
|     ></v-text-field> | ||||
| </template> | ||||
| 
 | ||||
| <style scoped> | ||||
| 
 | ||||
| </style> | ||||
|  | @ -2,5 +2,11 @@ | |||
|     "welcome": "Willkommen", | ||||
|     "error_title": "Fehler", | ||||
|     "previous": "Zurück", | ||||
|     "next": "Weiter" | ||||
|     "next": "Weiter", | ||||
|     "search": "Suchen...", | ||||
|     "yearsAge": "Jahre", | ||||
|     "enterSearchTerm": "Lernpfade suchen", | ||||
|     "enterSearchTermDescription": "Bitte geben Sie einen Suchbegriff ein.", | ||||
|     "noLearningPathsFound": "Nichts gefunden!", | ||||
|     "noLearningPathsFoundDescription": "Es gibt keine Lernpfade, die zu Ihrem Suchbegriff passen." | ||||
| } | ||||
|  |  | |||
|  | @ -8,5 +8,11 @@ | |||
|     "logout": "log out", | ||||
|     "error_title": "Error", | ||||
|     "previous": "Previous", | ||||
|     "next": "Next" | ||||
|     "next": "Next", | ||||
|     "search": "Search...", | ||||
|     "yearsAge": "years", | ||||
|     "enterSearchTerm": "Search learning paths", | ||||
|     "enterSearchTermDescription": "Please enter a search term.", | ||||
|     "noLearningPathsFound": "Nothing found!", | ||||
|     "noLearningPathsFoundDescription": "There are no learning paths matching your search term." | ||||
| } | ||||
|  |  | |||
|  | @ -2,5 +2,12 @@ | |||
|     "welcome": "Bienvenue", | ||||
|     "error_title": "Erreur", | ||||
|     "previous": "Précédente", | ||||
|     "next": "Suivante" | ||||
|     "next": "Suivante", | ||||
|     "search": "Réchercher...", | ||||
|     "yearsAge": "ans", | ||||
|     "enterSearchTerm": "Rechercher des parcours d'apprentissage", | ||||
|     "enterSearchTermDescription": "Saisissez un terme de recherche pour commencer.", | ||||
|     "noLearningPathsFound": "Rien trouvé !", | ||||
|     "noLearningPathsFoundDescription": "Aucun parcours d'apprentissage ne correspond à votre recherche." | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -8,5 +8,11 @@ | |||
|     "logout": "log uit", | ||||
|     "error_title": "Fout", | ||||
|     "previous": "Vorige", | ||||
|     "next": "Volgende" | ||||
|     "next": "Volgende", | ||||
|     "search": "Zoeken...", | ||||
|     "yearsAge": "jaar", | ||||
|     "enterSearchTerm": "Zoek naar leerpaden", | ||||
|     "enterSearchTermDescription": "Gelieve een zoekterm in te voeren.", | ||||
|     "noLearningPathsFound": "Niets gevonden!", | ||||
|     "noLearningPathsFoundDescription": "Er zijn geen leerpaden die overeenkomen met je zoekterm." | ||||
| } | ||||
|  |  | |||
|  | @ -15,6 +15,7 @@ import UserAssignments from "@/views/classes/UserAssignments.vue"; | |||
| import authState from "@/services/auth/auth-service.ts"; | ||||
| import LearningPathPage from "@/views/learning-paths/LearningPathPage.vue"; | ||||
| import path from "path"; | ||||
| import LearningPathSearchPage from "@/views/learning-paths/LearningPathSearchPage.vue"; | ||||
| 
 | ||||
| const router = createRouter({ | ||||
|     history: createWebHistory(import.meta.env.BASE_URL), | ||||
|  | @ -101,6 +102,12 @@ const router = createRouter({ | |||
|             component: SingleDiscussion, | ||||
|             meta: { requiresAuth: true }, | ||||
|         }, | ||||
|         { | ||||
|            path: "/learningPath/search", | ||||
|            name: "LearningPathSearchPage", | ||||
|            component: LearningPathSearchPage, | ||||
|            meta: { requiresAuth: false } | ||||
|         }, | ||||
|         { | ||||
|             path: "/learningPath/:hruid/:language", | ||||
|             name: "LearningPath", | ||||
|  |  | |||
|  | @ -117,9 +117,10 @@ export class LearningPath { | |||
|     } | ||||
| 
 | ||||
|     static fromDTO(dto: LearningPathDTO): LearningPath { | ||||
|         let startNodeDto = dto.nodes.filter(it => it.start_node); | ||||
|         let startNodeDto = dto.nodes.filter(it => it.start_node === true); | ||||
|         if (startNodeDto.length !== 1) { | ||||
|             throw new Error(`Invalid learning path! Expected precisely one start node, but there were ${startNodeDto.length}.`); | ||||
|             throw new Error(`Invalid learning path: ${dto.hruid}/${dto.language}!
 | ||||
|                                 Expected precisely one start node, but there were ${startNodeDto.length}.`);
 | ||||
|         } | ||||
|         return new LearningPath( | ||||
|             dto.language, | ||||
|  | @ -130,7 +131,8 @@ export class LearningPath { | |||
|             dto.num_nodes_left, | ||||
|             dto.keywords.split(' '), | ||||
|             {min: dto.min_age, max: dto.max_age}, | ||||
|             LearningPathNode.fromDTOAndOtherNodes(startNodeDto[0], dto.nodes) | ||||
|             LearningPathNode.fromDTOAndOtherNodes(startNodeDto[0], dto.nodes), | ||||
|             dto.image | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -9,6 +9,7 @@ | |||
|     import {loadResource, remoteResource, type SuccessState} from "@/services/api-client/remote-resource.ts"; | ||||
|     import LearningObjectView from "@/views/learning-paths/LearningObjectView.vue"; | ||||
|     import {useI18n} from "vue-i18n"; | ||||
|     import LearningPathSearchField from "@/components/LearningPathSearchField.vue"; | ||||
| 
 | ||||
|     const router = useRouter(); | ||||
|     const route = useRoute(); | ||||
|  | @ -128,11 +129,17 @@ | |||
|                 </using-remote-resource> | ||||
|             </div> | ||||
|         </v-navigation-drawer> | ||||
|         <div class="control-bar-above-content"> | ||||
|             <v-btn | ||||
|                 :icon="navigationDrawerShown ? 'mdi-menu-open' : 'mdi-menu'" | ||||
|                 class="navigation-drawer-toggle-button" | ||||
|                 variant="plain" | ||||
|                 @click="navigationDrawerShown = !navigationDrawerShown"></v-btn> | ||||
|             <div class="search-field-container"> | ||||
|                 <learning-path-search-field></learning-path-search-field> | ||||
|             </div> | ||||
|         </div> | ||||
| 
 | ||||
|         <learning-object-view | ||||
|             :hruid="currentNode.learningobjectHruid" | ||||
|             :language="currentNode.language" | ||||
|  | @ -161,9 +168,15 @@ | |||
| </template> | ||||
| 
 | ||||
| <style scoped> | ||||
|     .navigation-drawer-toggle-button { | ||||
|         margin-bottom: -30px; | ||||
|     .search-field-container { | ||||
|         min-width: 250px; | ||||
|     } | ||||
|     .control-bar-above-content { | ||||
|         margin-left: 5px; | ||||
|         margin-right: 5px; | ||||
|         margin-bottom: -30px; | ||||
|         display: flex; | ||||
|         justify-content: space-between; | ||||
|     } | ||||
|     .navigation-buttons-container { | ||||
|         padding: 20px; | ||||
|  |  | |||
							
								
								
									
										89
									
								
								frontend/src/views/learning-paths/LearningPathSearchPage.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								frontend/src/views/learning-paths/LearningPathSearchPage.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,89 @@ | |||
| <script setup lang="ts"> | ||||
|     import {loadResource, remoteResource} from "@/services/api-client/remote-resource.ts"; | ||||
|     import type {LearningPath} from "@/services/learning-content/learning-path.ts"; | ||||
|     import {useRoute, useRouter} from "vue-router"; | ||||
|     import {computed, watch} from "vue"; | ||||
|     import {searchLearningPaths} from "@/services/learning-content/learning-path-service.ts"; | ||||
|     import {useI18n} from "vue-i18n"; | ||||
|     import UsingRemoteResource from "@/components/UsingRemoteResource.vue"; | ||||
|     import {convertBase64ToImageSrc} from "@/utils/base64ToImage.ts"; | ||||
|     import LearningPathSearchField from "@/components/LearningPathSearchField.vue"; | ||||
| 
 | ||||
|     const route = useRoute(); | ||||
|     const router = useRouter(); | ||||
|     const { t } = useI18n(); | ||||
| 
 | ||||
|     const query = computed(() => route.query.query as string | null); | ||||
| 
 | ||||
|     const searchResultsResource = remoteResource<LearningPath[]>(); | ||||
|     watch(query, () => { | ||||
|         if (query.value) { | ||||
|             loadResource(searchResultsResource, searchLearningPaths(query.value)) | ||||
|         } | ||||
|     }, {immediate: true}); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|     <div class="search-field-container"> | ||||
|         <learning-path-search-field></learning-path-search-field> | ||||
|     </div> | ||||
| 
 | ||||
|     <using-remote-resource :resource="searchResultsResource" v-slot="{ data }: {data: LearningPath[]}"> | ||||
|         <div class="results-grid" v-if="data.length > 0"> | ||||
|             <v-card | ||||
|                 class="learning-path-card" | ||||
|                 link | ||||
|                 :to="`${learningPath.hruid}/${learningPath.language}`" | ||||
|                 v-for="learningPath in data" | ||||
|             > | ||||
|                 <v-img | ||||
|                     height="300px" | ||||
|                     :src="convertBase64ToImageSrc(learningPath.image)" | ||||
|                     cover | ||||
|                     v-if="learningPath.image" | ||||
|                 ></v-img> | ||||
|                 <v-card-title>{{ learningPath.title }}</v-card-title> | ||||
|                 <v-card-subtitle> | ||||
|                     <v-icon icon="mdi-human-male-boy"></v-icon> | ||||
|                     <span>{{ learningPath.targetAges.min }} - {{ learningPath.targetAges.max }} {{ t('yearsAge') }}</span> | ||||
|                 </v-card-subtitle> | ||||
|                 <v-card-text>{{ learningPath.description }}</v-card-text> | ||||
|             </v-card> | ||||
|         </div> | ||||
|         <div content="empty-state-container" v-else> | ||||
|             <v-empty-state | ||||
|                 icon="mdi-emoticon-sad-outline" | ||||
|                 :title="t('noLearningPathsFound')" | ||||
|                 :text="t('noLearningPathsFoundDescription')" | ||||
|             ></v-empty-state> | ||||
|         </div> | ||||
|     </using-remote-resource> | ||||
|     <div content="empty-state-container"> | ||||
|         <v-empty-state | ||||
|             v-if="!query" | ||||
|             icon="mdi-magnify" | ||||
|             :title="t('enterSearchTerm')" | ||||
|             :text="t('enterSearchTermDescription')" | ||||
|         ></v-empty-state> | ||||
|     </div> | ||||
| </template> | ||||
| 
 | ||||
| <style scoped> | ||||
|     .search-field-container { | ||||
|         display: block; | ||||
|         margin: 20px; | ||||
|     } | ||||
|     .results-grid { | ||||
|         margin: 20px; | ||||
|         display: flex; | ||||
|         align-items: stretch; | ||||
|         gap: 20px; | ||||
|         flex-wrap: wrap; | ||||
|     } | ||||
|     .search-field { | ||||
|         max-width: 300px; | ||||
|     } | ||||
|     .learning-path-card { | ||||
|         width: 300px; | ||||
|     } | ||||
| </style> | ||||
		Reference in a new issue
	
	 Gerald Schmittinger
						Gerald Schmittinger