feat(vocab): implement pagination and localization for vocabulary dictionaries
All checks were successful
Deploy to production / deploy (push) Successful in 3m2s
All checks were successful
Deploy to production / deploy (push) Successful in 3m2s
- Added pagination functionality to the vocabulary dictionary views, allowing users to navigate through results efficiently. - Introduced a new method `_parseDictionaryPaging` in `VocabService` to handle pagination parameters. - Updated `getLanguageDictionary` and `getCourseDictionary` methods to return pagination details alongside results. - Enhanced the `VocabDictionaryView` component with pagination controls and updated UI for better user experience. - Added localization entries for pagination in Cebuano, German, English, Spanish, and French, ensuring a consistent user experience across languages.
This commit is contained in:
@@ -34,6 +34,15 @@ export default class VocabService {
|
||||
return Math.max(min, Math.min(max, Math.trunc(numeric)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Wörterbuch-API: page ab 1, pageSize max. 100, Standard 25.
|
||||
*/
|
||||
_parseDictionaryPaging(query = {}) {
|
||||
const page = Math.max(1, this._clampInteger(query?.page, { min: 1, max: 1_000_000, fallback: 1 }));
|
||||
const pageSize = this._clampInteger(query?.pageSize, { min: 1, max: 100, fallback: 25 });
|
||||
return { page, pageSize };
|
||||
}
|
||||
|
||||
_sanitizeShortString(value, maxLength = 400) {
|
||||
const text = String(value ?? '').trim();
|
||||
if (!text) {
|
||||
@@ -1609,42 +1618,77 @@ export default class VocabService {
|
||||
* Wörterbuch: alle Vokabeln einer Trainer-Sprache (Kapitel), optional gefiltert.
|
||||
* Ein Suchbegriff durchsucht Lern- und Referenzspalte (Teilstrings, ILIKE).
|
||||
*/
|
||||
async getLanguageDictionary(hashedUserId, languageId, { q } = {}) {
|
||||
async getLanguageDictionary(hashedUserId, languageId, query = {}) {
|
||||
const { q, page: pageParam, pageSize: pageSizeParam } = query;
|
||||
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 { page, pageSize } = this._parseDictionaryPaging({ page: pageParam, pageSize: pageSizeParam });
|
||||
|
||||
const rows = await sequelize.query(
|
||||
const baseReplacements = like ? { languageId: access.id, like } : { languageId: access.id };
|
||||
|
||||
const countRows = await sequelize.query(
|
||||
`
|
||||
SELECT
|
||||
cl.id,
|
||||
c.id AS "chapterId",
|
||||
c.title AS "chapterTitle",
|
||||
l1.text AS "learning",
|
||||
l2.text AS "reference"
|
||||
SELECT COUNT(*)::integer AS n
|
||||
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 },
|
||||
replacements: baseReplacements,
|
||||
type: sequelize.QueryTypes.SELECT,
|
||||
}
|
||||
);
|
||||
const total = countRows[0]?.n ?? 0;
|
||||
const totalPages = total === 0 ? 1 : Math.ceil(total / pageSize);
|
||||
const effectivePage = Math.min(Math.max(1, page), totalPages);
|
||||
const offset = (effectivePage - 1) * pageSize;
|
||||
|
||||
return { languageId: access.id, results: rows };
|
||||
let rows = [];
|
||||
if (total > 0) {
|
||||
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 :limit OFFSET :offset
|
||||
`,
|
||||
{
|
||||
replacements: { ...baseReplacements, limit: pageSize, offset },
|
||||
type: sequelize.QueryTypes.SELECT,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
languageId: access.id,
|
||||
results: rows,
|
||||
total,
|
||||
page: effectivePage,
|
||||
pageSize,
|
||||
totalPages,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Wörterbuch: aus abgeschlossenen Kurslektionen extrahierte Paare, optional gefiltert (Teilstring in beiden Spalten).
|
||||
*/
|
||||
async getCourseDictionary(hashedUserId, courseId, { q } = {}) {
|
||||
async getCourseDictionary(hashedUserId, courseId, query = {}) {
|
||||
const { q, page: pageParam, pageSize: pageSizeParam } = query;
|
||||
const pool = await this.getCompletedLessonVocabPool(hashedUserId, courseId);
|
||||
const term = typeof q === 'string' ? q.trim().toLowerCase() : '';
|
||||
let vocabs = pool.vocabs || [];
|
||||
@@ -1660,7 +1704,20 @@ export default class VocabService {
|
||||
if (refCmp !== 0) return refCmp;
|
||||
return String(a.learning || '').localeCompare(String(b.learning || ''), undefined, { sensitivity: 'base' });
|
||||
});
|
||||
return { courseId: pool.courseId, results: vocabs };
|
||||
const { page, pageSize } = this._parseDictionaryPaging({ page: pageParam, pageSize: pageSizeParam });
|
||||
const total = vocabs.length;
|
||||
const totalPages = total === 0 ? 1 : Math.ceil(total / pageSize);
|
||||
const effectivePage = Math.min(Math.max(1, page), totalPages);
|
||||
const offset = (effectivePage - 1) * pageSize;
|
||||
const paged = vocabs.slice(offset, offset + pageSize);
|
||||
return {
|
||||
courseId: pool.courseId,
|
||||
results: paged,
|
||||
total,
|
||||
page: effectivePage,
|
||||
pageSize,
|
||||
totalPages,
|
||||
};
|
||||
}
|
||||
|
||||
async addVocabToChapter(hashedUserId, chapterId, { learning, reference }) {
|
||||
|
||||
Reference in New Issue
Block a user