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>
|
Loading…
Add table
Add a link
Reference in a new issue