feat(backend): SingleTheme-pagina geïmplementeerd
This commit is contained in:
		
							parent
							
								
									a33ec6c452
								
							
						
					
					
						commit
						34f980d690
					
				
					 7 changed files with 137 additions and 49 deletions
				
			
		
							
								
								
									
										54
									
								
								frontend/src/components/LearningPathsGrid.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								frontend/src/components/LearningPathsGrid.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,54 @@ | |||
| <script setup lang="ts"> | ||||
| 
 | ||||
| import {convertBase64ToImageSrc} from "@/utils/base64ToImage.ts"; | ||||
| import type {LearningPath} from "@/data-objects/learning-path.ts"; | ||||
| import {useI18n} from "vue-i18n"; | ||||
| 
 | ||||
| const { t } = useI18n(); | ||||
| const props = defineProps<{learningPaths: LearningPath[]}>(); | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|     <div class="results-grid" v-if="props.learningPaths.length > 0"> | ||||
|         <v-card | ||||
|             class="learning-path-card" | ||||
|             link | ||||
|             :to="`/learningPath/${learningPath.hruid}/${learningPath.language}/${learningPath.startNode.learningobjectHruid}`" | ||||
|             v-for="learningPath in props.learningPaths" | ||||
|         > | ||||
|             <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> | ||||
| </template> | ||||
| 
 | ||||
| <style scoped> | ||||
|     .learning-path-card { | ||||
|         width: 300px; | ||||
|     } | ||||
|     .results-grid { | ||||
|         margin: 20px; | ||||
|         display: flex; | ||||
|         align-items: stretch; | ||||
|         gap: 20px; | ||||
|         flex-wrap: wrap; | ||||
|     } | ||||
| </style> | ||||
|  | @ -21,4 +21,8 @@ export class LearningPathController extends BaseController { | |||
|         }); | ||||
|         return LearningPath.fromDTO(single(dtos)); | ||||
|     } | ||||
|     async getAllByTheme(theme: string) { | ||||
|         let dtos = await this.get<LearningPathDTO[]>("/", {theme}); | ||||
|         return dtos.map(dto => LearningPath.fromDTO(dto)); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -1,6 +1,4 @@ | |||
| import type {Language} from "@/data-objects/language.ts"; | ||||
| import type {LearningObject} from "@/data-objects/learning-object.ts"; | ||||
| import {getLearningObjectMetadata} from "@/services/learning-content/learning-object-service.ts"; | ||||
| 
 | ||||
| export interface LearningPathDTO { | ||||
|     language: string; | ||||
|  | @ -54,10 +52,6 @@ export class LearningPathNode { | |||
|     ) { | ||||
|     } | ||||
| 
 | ||||
|     get learningObject(): Promise<LearningObject> { | ||||
|         return getLearningObjectMetadata(this.learningobjectHruid, this.language, this.version); | ||||
|     } | ||||
| 
 | ||||
|     static fromDTOAndOtherNodes(dto: LearningPathNodeDTO, otherNodes: LearningPathNodeDTO[]): LearningPathNode { | ||||
|         return new LearningPathNode( | ||||
|             dto.learningobject_hruid, | ||||
|  |  | |||
|  | @ -22,6 +22,18 @@ export function useGetLearningPathQuery( | |||
|     }) | ||||
| } | ||||
| 
 | ||||
| export function useGetAllLearningPathsByThemeQuery( | ||||
|     theme: MaybeRefOrGetter<string> | ||||
| ): UseQueryReturnType<LearningPath[], Error> { | ||||
|     return useQuery({ | ||||
|         queryKey: [LEARNING_PATH_KEY, "getAllByTheme", theme], | ||||
|         queryFn: () => { | ||||
|             return learningPathController.getAllByTheme(toValue(theme)) | ||||
|         }, | ||||
|         enabled: () => Boolean(toValue(theme)), | ||||
|     }) | ||||
| } | ||||
| 
 | ||||
| export function useSearchLearningPathQuery( | ||||
|     query: MaybeRefOrGetter<string> | ||||
| ): UseQueryReturnType<LearningPath[], Error>  { | ||||
|  |  | |||
|  | @ -65,9 +65,10 @@ const router = createRouter({ | |||
|         }, | ||||
| 
 | ||||
|         { | ||||
|             path: "/theme/:id", | ||||
|             path: "/theme/:theme", | ||||
|             name: "Theme", | ||||
|             component: SingleTheme, | ||||
|             props: true, | ||||
|             meta: { requiresAuth: true }, | ||||
|         }, | ||||
|         { | ||||
|  |  | |||
|  | @ -1,7 +1,67 @@ | |||
| <script setup lang="ts"></script> | ||||
| <script setup lang="ts"> | ||||
| import type {LearningPath} from "@/data-objects/learning-path.ts"; | ||||
| import LearningPathsGrid from "@/components/LearningPathsGrid.vue"; | ||||
| import UsingQueryResult from "@/components/UsingQueryResult.vue"; | ||||
| import {useGetAllLearningPathsByThemeQuery} from "@/queries/learning-paths.ts"; | ||||
| import {computed, ref} from "vue"; | ||||
| import {useI18n} from "vue-i18n"; | ||||
| import {useThemeQuery} from "@/queries/themes.ts"; | ||||
| 
 | ||||
| const props = defineProps<{theme: string}>(); | ||||
| 
 | ||||
| const { locale } = useI18n(); | ||||
| const language = computed(() => locale.value); | ||||
| 
 | ||||
| const themeQueryResult = useThemeQuery(language); | ||||
| 
 | ||||
| const currentThemeInfo = computed(() => | ||||
|     themeQueryResult.isSuccess.value ? themeQueryResult.data.value.filter(it => it.key === props.theme)[0] : undefined | ||||
| ); | ||||
| 
 | ||||
| const learningPathsForThemeQueryResult = useGetAllLearningPathsByThemeQuery(() => props.theme); | ||||
| 
 | ||||
| const { t } = useI18n(); | ||||
| const searchFilter = ref(""); | ||||
| 
 | ||||
| function filterLearningPaths(learningPaths: LearningPath[]) { | ||||
|     return learningPaths.filter(it => | ||||
|         it.title.toLowerCase().includes(searchFilter.value.toLowerCase()) | ||||
|         || it.description.toLowerCase().includes(searchFilter.value.toLowerCase) | ||||
|     ); | ||||
| } | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|     <main></main> | ||||
|     <div class="container"> | ||||
|         <using-query-result :query-result="themeQueryResult"> | ||||
|             <h1>{{ currentThemeInfo.title }}</h1> | ||||
|             <p>{{ currentThemeInfo.description }}</p> | ||||
|             <div class="search-field-container"> | ||||
|                 <v-text-field | ||||
|                     class="search-field" | ||||
|                     :label="t('search')" | ||||
|                     append-inner-icon="mdi-magnify" | ||||
|                     v-model="searchFilter" | ||||
|                 ></v-text-field> | ||||
|             </div> | ||||
| 
 | ||||
|             <using-query-result :query-result="learningPathsForThemeQueryResult" v-slot="{ data }: {data: LearningPath[]}"> | ||||
|                 <learning-paths-grid :learning-paths="filterLearningPaths(data)"></learning-paths-grid> | ||||
|             </using-query-result> | ||||
|         </using-query-result> | ||||
|     </div> | ||||
| </template> | ||||
| 
 | ||||
| <style scoped></style> | ||||
| <style scoped> | ||||
|     .search-field-container { | ||||
|         display: block; | ||||
|         margin: 20px; | ||||
|     } | ||||
|     .search-field { | ||||
|         max-width: 300px; | ||||
|     } | ||||
|     .container { | ||||
|         padding: 20px; | ||||
|     } | ||||
| </style> | ||||
|  |  | |||
|  | @ -3,10 +3,10 @@ | |||
|     import {useRoute, useRouter} from "vue-router"; | ||||
|     import {computed} from "vue"; | ||||
|     import {useI18n} from "vue-i18n"; | ||||
|     import {convertBase64ToImageSrc} from "@/utils/base64ToImage.ts"; | ||||
|     import LearningPathSearchField from "@/components/LearningPathSearchField.vue"; | ||||
|     import {useSearchLearningPathQuery} from "@/queries/learning-paths.ts"; | ||||
|     import UsingQueryResult from "@/components/UsingQueryResult.vue"; | ||||
|     import LearningPathsGrid from "@/components/LearningPathsGrid.vue"; | ||||
| 
 | ||||
|     const route = useRoute(); | ||||
|     const router = useRouter(); | ||||
|  | @ -23,34 +23,7 @@ | |||
|     </div> | ||||
| 
 | ||||
|     <using-query-result :query-result="searchQueryResults" 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}/${learningPath.startNode.learningobjectHruid}`" | ||||
|                 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> | ||||
|         <learning-paths-grid :learning-paths="data"></learning-paths-grid> | ||||
|     </using-query-result> | ||||
|     <div content="empty-state-container"> | ||||
|         <v-empty-state | ||||
|  | @ -67,17 +40,7 @@ | |||
|         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