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", |     "welcome": "Willkommen", | ||||||
|     "error_title": "Fehler", |     "error_title": "Fehler", | ||||||
|     "previous": "Zurück", |     "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", |     "logout": "log out", | ||||||
|     "error_title": "Error", |     "error_title": "Error", | ||||||
|     "previous": "Previous", |     "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", |     "welcome": "Bienvenue", | ||||||
|     "error_title": "Erreur", |     "error_title": "Erreur", | ||||||
|     "previous": "Précédente", |     "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", |     "logout": "log uit", | ||||||
|     "error_title": "Fout", |     "error_title": "Fout", | ||||||
|     "previous": "Vorige", |     "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 authState from "@/services/auth/auth-service.ts"; | ||||||
| import LearningPathPage from "@/views/learning-paths/LearningPathPage.vue"; | import LearningPathPage from "@/views/learning-paths/LearningPathPage.vue"; | ||||||
| import path from "path"; | import path from "path"; | ||||||
|  | import LearningPathSearchPage from "@/views/learning-paths/LearningPathSearchPage.vue"; | ||||||
| 
 | 
 | ||||||
| const router = createRouter({ | const router = createRouter({ | ||||||
|     history: createWebHistory(import.meta.env.BASE_URL), |     history: createWebHistory(import.meta.env.BASE_URL), | ||||||
|  | @ -101,6 +102,12 @@ const router = createRouter({ | ||||||
|             component: SingleDiscussion, |             component: SingleDiscussion, | ||||||
|             meta: { requiresAuth: true }, |             meta: { requiresAuth: true }, | ||||||
|         }, |         }, | ||||||
|  |         { | ||||||
|  |            path: "/learningPath/search", | ||||||
|  |            name: "LearningPathSearchPage", | ||||||
|  |            component: LearningPathSearchPage, | ||||||
|  |            meta: { requiresAuth: false } | ||||||
|  |         }, | ||||||
|         { |         { | ||||||
|             path: "/learningPath/:hruid/:language", |             path: "/learningPath/:hruid/:language", | ||||||
|             name: "LearningPath", |             name: "LearningPath", | ||||||
|  |  | ||||||
|  | @ -117,9 +117,10 @@ export class LearningPath { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     static fromDTO(dto: LearningPathDTO): 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) { |         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( |         return new LearningPath( | ||||||
|             dto.language, |             dto.language, | ||||||
|  | @ -130,7 +131,8 @@ export class LearningPath { | ||||||
|             dto.num_nodes_left, |             dto.num_nodes_left, | ||||||
|             dto.keywords.split(' '), |             dto.keywords.split(' '), | ||||||
|             {min: dto.min_age, max: dto.max_age}, |             {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 {loadResource, remoteResource, type SuccessState} from "@/services/api-client/remote-resource.ts"; | ||||||
|     import LearningObjectView from "@/views/learning-paths/LearningObjectView.vue"; |     import LearningObjectView from "@/views/learning-paths/LearningObjectView.vue"; | ||||||
|     import {useI18n} from "vue-i18n"; |     import {useI18n} from "vue-i18n"; | ||||||
|  |     import LearningPathSearchField from "@/components/LearningPathSearchField.vue"; | ||||||
| 
 | 
 | ||||||
|     const router = useRouter(); |     const router = useRouter(); | ||||||
|     const route = useRoute(); |     const route = useRoute(); | ||||||
|  | @ -128,11 +129,17 @@ | ||||||
|                 </using-remote-resource> |                 </using-remote-resource> | ||||||
|             </div> |             </div> | ||||||
|         </v-navigation-drawer> |         </v-navigation-drawer> | ||||||
|         <v-btn |         <div class="control-bar-above-content"> | ||||||
|             :icon="navigationDrawerShown ? 'mdi-menu-open' : 'mdi-menu'" |             <v-btn | ||||||
|             class="navigation-drawer-toggle-button" |                 :icon="navigationDrawerShown ? 'mdi-menu-open' : 'mdi-menu'" | ||||||
|             variant="plain" |                 class="navigation-drawer-toggle-button" | ||||||
|             @click="navigationDrawerShown = !navigationDrawerShown"></v-btn> |                 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 |         <learning-object-view | ||||||
|             :hruid="currentNode.learningobjectHruid" |             :hruid="currentNode.learningobjectHruid" | ||||||
|             :language="currentNode.language" |             :language="currentNode.language" | ||||||
|  | @ -161,9 +168,15 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <style scoped> | <style scoped> | ||||||
|     .navigation-drawer-toggle-button { |     .search-field-container { | ||||||
|         margin-bottom: -30px; |         min-width: 250px; | ||||||
|  |     } | ||||||
|  |     .control-bar-above-content { | ||||||
|         margin-left: 5px; |         margin-left: 5px; | ||||||
|  |         margin-right: 5px; | ||||||
|  |         margin-bottom: -30px; | ||||||
|  |         display: flex; | ||||||
|  |         justify-content: space-between; | ||||||
|     } |     } | ||||||
|     .navigation-buttons-container { |     .navigation-buttons-container { | ||||||
|         padding: 20px; |         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