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)); |         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 {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 { | export interface LearningPathDTO { | ||||||
|     language: string; |     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 { |     static fromDTOAndOtherNodes(dto: LearningPathNodeDTO, otherNodes: LearningPathNodeDTO[]): LearningPathNode { | ||||||
|         return new LearningPathNode( |         return new LearningPathNode( | ||||||
|             dto.learningobject_hruid, |             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( | export function useSearchLearningPathQuery( | ||||||
|     query: MaybeRefOrGetter<string> |     query: MaybeRefOrGetter<string> | ||||||
| ): UseQueryReturnType<LearningPath[], Error>  { | ): UseQueryReturnType<LearningPath[], Error>  { | ||||||
|  |  | ||||||
|  | @ -65,9 +65,10 @@ const router = createRouter({ | ||||||
|         }, |         }, | ||||||
| 
 | 
 | ||||||
|         { |         { | ||||||
|             path: "/theme/:id", |             path: "/theme/:theme", | ||||||
|             name: "Theme", |             name: "Theme", | ||||||
|             component: SingleTheme, |             component: SingleTheme, | ||||||
|  |             props: true, | ||||||
|             meta: { requiresAuth: 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> | <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> | </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 {useRoute, useRouter} from "vue-router"; | ||||||
|     import {computed} from "vue"; |     import {computed} from "vue"; | ||||||
|     import {useI18n} from "vue-i18n"; |     import {useI18n} from "vue-i18n"; | ||||||
|     import {convertBase64ToImageSrc} from "@/utils/base64ToImage.ts"; |  | ||||||
|     import LearningPathSearchField from "@/components/LearningPathSearchField.vue"; |     import LearningPathSearchField from "@/components/LearningPathSearchField.vue"; | ||||||
|     import {useSearchLearningPathQuery} from "@/queries/learning-paths.ts"; |     import {useSearchLearningPathQuery} from "@/queries/learning-paths.ts"; | ||||||
|     import UsingQueryResult from "@/components/UsingQueryResult.vue"; |     import UsingQueryResult from "@/components/UsingQueryResult.vue"; | ||||||
|  |     import LearningPathsGrid from "@/components/LearningPathsGrid.vue"; | ||||||
| 
 | 
 | ||||||
|     const route = useRoute(); |     const route = useRoute(); | ||||||
|     const router = useRouter(); |     const router = useRouter(); | ||||||
|  | @ -23,34 +23,7 @@ | ||||||
|     </div> |     </div> | ||||||
| 
 | 
 | ||||||
|     <using-query-result :query-result="searchQueryResults" v-slot="{ data }: {data: LearningPath[]}"> |     <using-query-result :query-result="searchQueryResults" v-slot="{ data }: {data: LearningPath[]}"> | ||||||
|         <div class="results-grid" v-if="data.length > 0"> |         <learning-paths-grid :learning-paths="data"></learning-paths-grid> | ||||||
|             <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> |  | ||||||
|     </using-query-result> |     </using-query-result> | ||||||
|     <div content="empty-state-container"> |     <div content="empty-state-container"> | ||||||
|         <v-empty-state |         <v-empty-state | ||||||
|  | @ -67,17 +40,7 @@ | ||||||
|         display: block; |         display: block; | ||||||
|         margin: 20px; |         margin: 20px; | ||||||
|     } |     } | ||||||
|     .results-grid { |  | ||||||
|         margin: 20px; |  | ||||||
|         display: flex; |  | ||||||
|         align-items: stretch; |  | ||||||
|         gap: 20px; |  | ||||||
|         flex-wrap: wrap; |  | ||||||
|     } |  | ||||||
|     .search-field { |     .search-field { | ||||||
|         max-width: 300px; |         max-width: 300px; | ||||||
|     } |     } | ||||||
|     .learning-path-card { |  | ||||||
|         width: 300px; |  | ||||||
|     } |  | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
		Reference in a new issue
	
	 Gerald Schmittinger
						Gerald Schmittinger