From f5e3a9a4a29681fe5410680f324965f7757759ad Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Mon, 5 Jan 2026 16:53:38 +0100 Subject: [PATCH] Add search functionality for vocabulary in VocabController and VocabService - Implemented a new searchVocabs method in VocabService to allow users to search for vocabulary based on learning and mother tongue terms. - Updated VocabController to include the searchVocabs method wrapped with user authentication. - Added a new route in vocabRouter for searching vocabulary by language ID. - Enhanced VocabChapterView and VocabLanguageView components to include a button for opening the search dialog. - Added translations for search-related terms in both German and English locales, improving user accessibility. --- backend/controllers/vocabController.js | 1 + backend/routers/vocabRouter.js | 1 + backend/services/vocabService.js | 46 +++++ .../socialnetwork/VocabSearchDialog.vue | 170 ++++++++++++++++++ .../src/i18n/locales/de/socialnetwork.json | 10 ++ .../src/i18n/locales/en/socialnetwork.json | 10 ++ .../src/views/social/VocabChapterView.vue | 18 +- .../src/views/social/VocabLanguageView.vue | 11 ++ 8 files changed, 266 insertions(+), 1 deletion(-) create mode 100644 frontend/src/dialogues/socialnetwork/VocabSearchDialog.vue diff --git a/backend/controllers/vocabController.js b/backend/controllers/vocabController.js index 71f9abd..d4d4e44 100644 --- a/backend/controllers/vocabController.js +++ b/backend/controllers/vocabController.js @@ -16,6 +16,7 @@ class VocabController { this.listChapters = this._wrapWithUser((userId, req) => this.service.listChapters(userId, req.params.languageId)); this.createChapter = this._wrapWithUser((userId, req) => this.service.createChapter(userId, req.params.languageId, req.body), { successStatus: 201 }); this.listLanguageVocabs = this._wrapWithUser((userId, req) => this.service.listLanguageVocabs(userId, req.params.languageId)); + this.searchVocabs = this._wrapWithUser((userId, req) => this.service.searchVocabs(userId, req.params.languageId, req.query)); this.getChapter = this._wrapWithUser((userId, req) => this.service.getChapter(userId, req.params.chapterId)); this.listChapterVocabs = this._wrapWithUser((userId, req) => this.service.listChapterVocabs(userId, req.params.chapterId)); diff --git a/backend/routers/vocabRouter.js b/backend/routers/vocabRouter.js index 78ce1c4..2c31f01 100644 --- a/backend/routers/vocabRouter.js +++ b/backend/routers/vocabRouter.js @@ -16,6 +16,7 @@ router.get('/languages/:languageId', vocabController.getLanguage); router.get('/languages/:languageId/chapters', vocabController.listChapters); router.post('/languages/:languageId/chapters', vocabController.createChapter); router.get('/languages/:languageId/vocabs', vocabController.listLanguageVocabs); +router.get('/languages/:languageId/search', vocabController.searchVocabs); router.get('/chapters/:chapterId', vocabController.getChapter); router.get('/chapters/:chapterId/vocabs', vocabController.listChapterVocabs); diff --git a/backend/services/vocabService.js b/backend/services/vocabService.js index 534f2b2..4a2fcb9 100644 --- a/backend/services/vocabService.js +++ b/backend/services/vocabService.js @@ -408,6 +408,52 @@ export default class VocabService { return { languageId: access.id, isOwner: access.isOwner, vocabs: rows }; } + async searchVocabs(hashedUserId, languageId, { learning = '', motherTongue = '' } = {}) { + const user = await this._getUserByHashedId(hashedUserId); + const access = await this._getLanguageAccess(user.id, languageId); + + const learningTerm = typeof learning === 'string' ? learning.trim() : ''; + const motherTerm = typeof motherTongue === 'string' ? motherTongue.trim() : ''; + if (!learningTerm && !motherTerm) { + const err = new Error('Missing search term'); + err.status = 400; + throw err; + } + + const learningLike = learningTerm ? `%${learningTerm}%` : null; + const motherLike = motherTerm ? `%${motherTerm}%` : null; + + const rows = await sequelize.query( + ` + SELECT + cl.id, + c.id AS "chapterId", + c.title AS "chapterTitle", + l1.text AS "learning", + l2.text AS "motherTongue" + FROM community.vocab_chapter_lexeme cl + JOIN community.vocab_chapter c ON c.id = cl.chapter_id + JOIN community.vocab_lexeme l1 ON l1.id = cl.learning_lexeme_id + JOIN community.vocab_lexeme l2 ON l2.id = cl.reference_lexeme_id + WHERE c.language_id = :languageId + AND (:learningLike IS NULL OR l1.text ILIKE :learningLike) + AND (:motherLike IS NULL OR l2.text ILIKE :motherLike) + ORDER BY l2.text ASC, l1.text ASC, c.title ASC + LIMIT 200 + `, + { + replacements: { + languageId: access.id, + learningLike, + motherLike, + }, + type: sequelize.QueryTypes.SELECT, + } + ); + + return { languageId: access.id, results: rows }; + } + async addVocabToChapter(hashedUserId, chapterId, { learning, reference }) { const user = await this._getUserByHashedId(hashedUserId); const ch = await this._getChapterAccess(user.id, chapterId); diff --git a/frontend/src/dialogues/socialnetwork/VocabSearchDialog.vue b/frontend/src/dialogues/socialnetwork/VocabSearchDialog.vue new file mode 100644 index 0000000..6383b83 --- /dev/null +++ b/frontend/src/dialogues/socialnetwork/VocabSearchDialog.vue @@ -0,0 +1,170 @@ + + + + + + + diff --git a/frontend/src/i18n/locales/de/socialnetwork.json b/frontend/src/i18n/locales/de/socialnetwork.json index 1e54681..2add768 100644 --- a/frontend/src/i18n/locales/de/socialnetwork.json +++ b/frontend/src/i18n/locales/de/socialnetwork.json @@ -308,6 +308,16 @@ "stats": "Statistik", "success": "Erfolg", "fail": "Misserfolg" + }, + "search": { + "open": "Suche", + "title": "Vokabeln suchen", + "motherTongue": "Muttersprache", + "learningLanguage": "Lernsprache", + "lesson": "Lektion", + "search": "Suchen", + "noResults": "Keine Treffer.", + "error": "Suche fehlgeschlagen." } } } diff --git a/frontend/src/i18n/locales/en/socialnetwork.json b/frontend/src/i18n/locales/en/socialnetwork.json index 2e09e05..2b5844e 100644 --- a/frontend/src/i18n/locales/en/socialnetwork.json +++ b/frontend/src/i18n/locales/en/socialnetwork.json @@ -308,6 +308,16 @@ "stats": "Stats", "success": "Success", "fail": "Fail" + }, + "search": { + "open": "Search", + "title": "Search vocabulary", + "motherTongue": "Mother tongue", + "learningLanguage": "Learning language", + "lesson": "Lesson", + "search": "Search", + "noResults": "No results.", + "error": "Search failed." } } } diff --git a/frontend/src/views/social/VocabChapterView.vue b/frontend/src/views/social/VocabChapterView.vue index d7cf7cd..ba76963 100644 --- a/frontend/src/views/social/VocabChapterView.vue +++ b/frontend/src/views/social/VocabChapterView.vue @@ -9,6 +9,7 @@
+
@@ -50,21 +51,24 @@
+