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