feat(vocab): add language and course dictionary endpoints and UI components
All checks were successful
Deploy to production / deploy (push) Successful in 2m53s

- Implemented `getLanguageDictionary` and `getCourseDictionary` methods in the VocabService to retrieve vocabulary entries filtered by search terms.
- Updated VocabController and vocabRouter to include new routes for accessing language and course dictionaries.
- Enhanced frontend components to navigate to the new dictionary views, including buttons in VocabCourseView and VocabLanguageView.
- Added localization entries for the dictionary feature in multiple languages, ensuring a consistent user experience across the platform.
This commit is contained in:
Torsten Schulz (local)
2026-04-10 13:08:18 +02:00
parent 9582e7b900
commit d17c8a341d
18 changed files with 443 additions and 16 deletions

View File

@@ -1605,6 +1605,64 @@ export default class VocabService {
return { languageId: access.id, results: rows };
}
/**
* Wörterbuch: alle Vokabeln einer Trainer-Sprache (Kapitel), optional gefiltert.
* Ein Suchbegriff durchsucht Lern- und Referenzspalte (Teilstrings, ILIKE).
*/
async getLanguageDictionary(hashedUserId, languageId, { q } = {}) {
const user = await this._getUserByHashedId(hashedUserId);
const access = await this._getLanguageAccess(user.id, languageId);
const term = typeof q === 'string' ? q.trim() : '';
const like = term ? `%${term}%` : null;
const rows = await sequelize.query(
`
SELECT
cl.id,
c.id AS "chapterId",
c.title AS "chapterTitle",
l1.text AS "learning",
l2.text AS "reference"
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
${like ? 'AND (l1.text ILIKE :like OR l2.text ILIKE :like)' : ''}
ORDER BY c.title ASC, l1.text ASC, l2.text ASC
LIMIT 20000
`,
{
replacements: like ? { languageId: access.id, like } : { languageId: access.id },
type: sequelize.QueryTypes.SELECT,
}
);
return { languageId: access.id, results: rows };
}
/**
* Wörterbuch: aus abgeschlossenen Kurslektionen extrahierte Paare, optional gefiltert (Teilstring in beiden Spalten).
*/
async getCourseDictionary(hashedUserId, courseId, { q } = {}) {
const pool = await this.getCompletedLessonVocabPool(hashedUserId, courseId);
const term = typeof q === 'string' ? q.trim().toLowerCase() : '';
let vocabs = pool.vocabs || [];
if (term) {
vocabs = vocabs.filter((entry) => {
const l = String(entry.learning || '').toLowerCase();
const r = String(entry.reference || '').toLowerCase();
return l.includes(term) || r.includes(term);
});
}
vocabs.sort((a, b) => {
const refCmp = String(a.reference || '').localeCompare(String(b.reference || ''), undefined, { sensitivity: 'base' });
if (refCmp !== 0) return refCmp;
return String(a.learning || '').localeCompare(String(b.learning || ''), undefined, { sensitivity: 'base' });
});
return { courseId: pool.courseId, results: vocabs };
}
async addVocabToChapter(hashedUserId, chapterId, { learning, reference }) {
const user = await this._getUserByHashedId(hashedUserId);
const ch = await this._getChapterAccess(user.id, chapterId);