feat(frontend): Zoekfunctie voor leerpaden geïmplementeerd

This commit is contained in:
Gerald Schmittinger 2025-03-25 01:49:01 +01:00
parent a9643838b7
commit f9e2166504
9 changed files with 186 additions and 14 deletions

View 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>

View file

@ -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."
}

View file

@ -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."
}

View file

@ -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."
}

View file

@ -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."
}

View file

@ -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",

View file

@ -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
)
}
}

View file

@ -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;

View 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>