feat(vocab-lesson): implement pagination for vocabulary overview in lesson view
All checks were successful
Deploy to production / deploy (push) Successful in 2m59s

- Added pagination functionality to the vocabulary overview in the VocabLessonView component, allowing users to navigate through vocabulary items more efficiently.
- Introduced new localization entries for pagination controls in Cebuano, German, English, Spanish, and French, ensuring a consistent user experience across languages.
- Enhanced the UI with buttons for previous and next navigation, improving accessibility and usability of the vocabulary list.
This commit is contained in:
Torsten Schulz (local)
2026-04-10 14:26:01 +02:00
parent 545314e905
commit f46c864bbc
6 changed files with 122 additions and 2 deletions

View File

@@ -617,6 +617,9 @@
"deepenSectionTitle": "Deepen ug review",
"assistantSectionTitle": "Deepen uban sa pinulongan katabang",
"vocabOverviewToggle": "Show full overview sa terms",
"vocabOverviewPrev": "Balik",
"vocabOverviewNext": "Sunod",
"vocabOverviewPager": "{from}{to} sa {total} · Page {page} sa {pages}",
"vocabTrainerLockedHint": "Palihog confirm two preparation steps under “Preparation sa wala pa ang bokabularyo trainer” una.",
"exerciseUnlockHintAfterPrep": "Work through ang prepared terms una. Ang tsek sa kapitulo will unlock afterwards.",
"speakingTasks": "Speaking Tasks",

View File

@@ -586,6 +586,9 @@
"deepenSectionTitle": "Vertiefen und nachlesen",
"assistantSectionTitle": "Mit Sprachassistent vertiefen",
"vocabOverviewToggle": "Gesamtübersicht der Begriffe anzeigen",
"vocabOverviewPrev": "Zurück",
"vocabOverviewNext": "Weiter",
"vocabOverviewPager": "{from}{to} von {total} · Seite {page} von {pages}",
"vocabTrainerLockedHint": "Bitte bestätige zuerst zwei Lern-Durchgänge bei „Vorbereitung vor dem Vokabeltrainer“.",
"exerciseUnlockHintAfterPrep": "Arbeite zuerst die vorbereiteten Begriffe durch. Danach wird die Kapitel-Prüfung freigeschaltet.",
"speakingTasks": "Sprechaufträge",

View File

@@ -586,6 +586,9 @@
"deepenSectionTitle": "Deepen and review",
"assistantSectionTitle": "Deepen with language assistant",
"vocabOverviewToggle": "Show full overview of terms",
"vocabOverviewPrev": "Previous",
"vocabOverviewNext": "Next",
"vocabOverviewPager": "{from}{to} of {total} · Page {page} of {pages}",
"vocabTrainerLockedHint": "Please confirm two preparation steps under “Preparation before the vocabulary trainer” first.",
"exerciseUnlockHintAfterPrep": "Work through the prepared terms first. The chapter test will unlock afterwards.",
"speakingTasks": "Speaking Tasks",

View File

@@ -584,6 +584,9 @@
"deepenSectionTitle": "Profundizar y repasar",
"assistantSectionTitle": "Profundizar con el asistente de idiomas",
"vocabOverviewToggle": "Mostrar vista general completa de los términos",
"vocabOverviewPrev": "Anterior",
"vocabOverviewNext": "Siguiente",
"vocabOverviewPager": "{from}{to} de {total} · Página {page} de {pages}",
"vocabTrainerLockedHint": "Confirma primero los dos pasos de preparación arriba.",
"exerciseUnlockHintAfterPrep": "Primero recorre los términos preparados. Después se desbloqueará la prueba del capítulo.",
"speakingTasks": "Tareas orales",

View File

@@ -584,6 +584,9 @@
"deepenSectionTitle": "Approfondissez et lisez",
"assistantSectionTitle": "Approfondissez avec l'assistant vocal",
"vocabOverviewToggle": "Afficher un aperçu complet des termes",
"vocabOverviewPrev": "Précédent",
"vocabOverviewNext": "Suivant",
"vocabOverviewPager": "{from}{to} sur {total} · Page {page} sur {pages}",
"vocabTrainerLockedHint": "Veuillez d'abord confirmer deux séances d'apprentissage dans « Préparation avant l'entraîneur de vocabulaire ».",
"exerciseUnlockHintAfterPrep": "Commencez par parcourir les termes préparés. Lexamen du chapitre sera alors débloqué.",
"speakingTasks": "Travaux oraux",

View File

@@ -156,12 +156,31 @@
</summary>
<p class="vocab-info-text">{{ $t('socialnetwork.vocab.courses.vocabInfoText') }}</p>
<div class="vocab-items">
<div v-for="(vocab, index) in lessonVocab" :key="index" class="vocab-item">
<div v-for="(vocab, index) in paginatedLessonVocab" :key="`v-${vocabOverviewPagerMeta.page}-${index}`" class="vocab-item">
<strong>{{ vocab.learning || '—' }}</strong>
<span class="separator"></span>
<span>{{ vocab.reference }}</span>
</div>
</div>
<div v-if="vocabOverviewTotalPages > 1" class="vocab-items-pager">
<button
type="button"
class="vocab-items-pager__btn"
:disabled="vocabOverviewPagerMeta.page <= 1"
@click="vocabOverviewGoPrev"
>
{{ $t('socialnetwork.vocab.courses.vocabOverviewPrev') }}
</button>
<span class="vocab-items-pager__meta">{{ $t('socialnetwork.vocab.courses.vocabOverviewPager', vocabOverviewPagerMeta) }}</span>
<button
type="button"
class="vocab-items-pager__btn"
:disabled="vocabOverviewPagerMeta.page >= vocabOverviewPagerMeta.pages"
@click="vocabOverviewGoNext"
>
{{ $t('socialnetwork.vocab.courses.vocabOverviewNext') }}
</button>
</div>
</details>
<div v-if="visibleGrammarExplanations.length > 0" class="lesson-grammar-impulse didactic-card">
@@ -957,6 +976,8 @@ const LESSON_STATE_VERSION = 1;
const VOCAB_REPEAT_INTERVALS = [1, 2, 4];
/** Mindest-Erfolgsquote im Vokabeltrainer (gesamt), damit die Kapitel-Prüfung freigeschaltet wird. */
const EXERCISE_UNLOCK_MIN_SUCCESS_PERCENT = 70;
/** Max. Zeilen pro Seite in der einklappbaren Vokabel-Gesamtübersicht */
const VOCAB_OVERVIEW_PAGE_SIZE = 40;
export default {
name: 'VocabLessonView',
@@ -1046,7 +1067,9 @@ export default {
lessonStateSaveTimer: null,
lessonStateSaveInFlight: false,
pendingLessonStatePayload: null,
resettingLessonProgress: false
resettingLessonProgress: false,
/** Seitennummer (1-basiert) für die Vokabel-Gesamtübersicht */
vocabOverviewPage: 1
};
},
computed: {
@@ -1347,6 +1370,31 @@ export default {
return Array.from(vocabByReference.values());
},
vocabOverviewTotalPages() {
const n = this.lessonVocab.length;
if (n === 0) {
return 1;
}
return Math.ceil(n / VOCAB_OVERVIEW_PAGE_SIZE);
},
paginatedLessonVocab() {
const list = this.lessonVocab;
const totalPages = this.vocabOverviewTotalPages;
const page = Math.min(Math.max(1, this.vocabOverviewPage), totalPages);
const start = (page - 1) * VOCAB_OVERVIEW_PAGE_SIZE;
return list.slice(start, start + VOCAB_OVERVIEW_PAGE_SIZE);
},
vocabOverviewPagerMeta() {
const n = this.lessonVocab.length;
const totalPages = this.vocabOverviewTotalPages;
const page = Math.min(Math.max(1, this.vocabOverviewPage), totalPages);
if (n === 0) {
return { from: 0, to: 0, page: 1, pages: 1, total: 0 };
}
const from = (page - 1) * VOCAB_OVERVIEW_PAGE_SIZE + 1;
const to = Math.min(page * VOCAB_OVERVIEW_PAGE_SIZE, n);
return { from, to, page, pages: totalPages, total: n };
},
trainableLessonVocab() {
return this.lessonVocab.filter((entry) => entry.learning && entry.reference && entry.learning !== entry.reference);
},
@@ -1598,9 +1646,28 @@ export default {
this.isNavigatingToNext = false;
this.loadLesson();
}
},
lessonVocab: {
handler() {
const totalPages = this.vocabOverviewTotalPages;
if (this.vocabOverviewPage > totalPages) {
this.vocabOverviewPage = Math.max(1, totalPages);
}
},
deep: true
}
},
methods: {
vocabOverviewGoPrev() {
if (this.vocabOverviewPagerMeta.page > 1) {
this.vocabOverviewPage -= 1;
}
},
vocabOverviewGoNext() {
if (this.vocabOverviewPagerMeta.page < this.vocabOverviewPagerMeta.pages) {
this.vocabOverviewPage += 1;
}
},
exportPersistedExerciseAnswers() {
const exportedAnswers = {};
this.effectiveExercises.forEach((exercise) => {
@@ -2267,6 +2334,7 @@ export default {
this.exerciseRetryPending = false;
this.exerciseRetryPendingSinceAttempts = 0;
this.exerciseSequentialIndex = 0;
this.vocabOverviewPage = 1;
this.exercisePreparationCompleted = false;
this.lessonPrepStage = 0;
this.lessonPrepIndex = 0;
@@ -4548,6 +4616,43 @@ export default {
margin: 0 10px;
}
.vocab-items-pager {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 12px 16px;
margin-top: 14px;
padding-top: 12px;
border-top: 1px solid #eadfcf;
}
.vocab-items-pager__btn {
padding: 6px 14px;
font-size: 0.9rem;
border: 1px solid #c9b896;
border-radius: 8px;
background: #faf8f3;
color: #333;
cursor: pointer;
}
.vocab-items-pager__btn:hover:not(:disabled) {
background: #f0ebe0;
}
.vocab-items-pager__btn:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.vocab-items-pager__meta {
font-size: 0.88rem;
color: #555;
text-align: center;
min-width: min(100%, 14rem);
}
.grammar-explanations {
margin: 20px 0;
padding: 15px;