From d85420070829eeb7088ed683621e54a2f80dd626 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Sun, 26 Apr 2026 23:32:18 +0200 Subject: [PATCH] feat(VocabPracticeDialog, VocabCourseView): enhance hard vocabulary management and UI - Refactored methods in VocabPracticeDialog to improve the handling of hard vocabulary items, including the addition of `findMatchingHardKey` and `removeHardEntriesForItem` for better management of hard vocabulary entries. - Updated the VocabCourseView to display a new section for hard vocabulary items, allowing users to view and remove difficult words easily. - Enhanced the UI with new styles for the hard vocabulary list, improving user engagement and accessibility to challenging vocabulary practice. --- .../socialnetwork/VocabPracticeDialog.vue | 77 +++++++++++++--- frontend/src/views/social/VocabCourseView.vue | 90 ++++++++++++++++++- 2 files changed, 151 insertions(+), 16 deletions(-) diff --git a/frontend/src/dialogues/socialnetwork/VocabPracticeDialog.vue b/frontend/src/dialogues/socialnetwork/VocabPracticeDialog.vue index fce1b76..853fc9a 100644 --- a/frontend/src/dialogues/socialnetwork/VocabPracticeDialog.vue +++ b/frontend/src/dialogues/socialnetwork/VocabPracticeDialog.vue @@ -297,11 +297,11 @@ export default { }, isCurrentMarkedHard() { if (!this.current) return false; - return Boolean(this.hardVocabMap[this.getHardKey(this.current)]); + return this.isItemMarkedHard(this.current); }, hardPoolItems() { if (!Array.isArray(this.pool) || this.pool.length === 0) return []; - return this.pool.filter((item) => Boolean(this.hardVocabMap[this.getHardKey(item)])); + return this.pool.filter((item) => this.isItemMarkedHard(item)); }, hardRemainingCount() { return this.hardPoolItems.filter((item) => { @@ -344,8 +344,63 @@ export default { const reference = this.normalize(item?.reference || ''); return `${learning}|${reference}`; }, + findMatchingHardKey(item) { + const exactKey = this.getHardKey(item); + if (this.hardVocabMap[exactKey]) return exactKey; + + const itemLearning = this.normalize(item?.learning || ''); + const itemReference = this.normalize(item?.reference || ''); + if (!itemLearning || !itemReference) return null; + + const entries = Object.entries(this.hardVocabMap || {}); + for (const [key, value] of entries) { + const learningAlts = this.splitPhraseAlternatives(value?.learning || '') + .map((part) => this.normalize(part)) + .filter(Boolean); + const referenceAlts = this.splitPhraseAlternatives(value?.reference || '') + .map((part) => this.normalize(part)) + .filter(Boolean); + if (!learningAlts.length || !referenceAlts.length) continue; + if (learningAlts.includes(itemLearning) && referenceAlts.includes(itemReference)) { + return key; + } + } + return null; + }, + isItemMarkedHard(item) { + return Boolean(this.findMatchingHardKey(item)); + }, + removeHardEntriesForItem(item) { + const itemLearning = this.normalize(item?.learning || ''); + const itemReference = this.normalize(item?.reference || ''); + if (!itemLearning || !itemReference) return; + + const toDelete = []; + Object.entries(this.hardVocabMap || {}).forEach(([key, value]) => { + const learningAlts = this.splitPhraseAlternatives(value?.learning || '') + .map((part) => this.normalize(part)) + .filter(Boolean); + const referenceAlts = this.splitPhraseAlternatives(value?.reference || '') + .map((part) => this.normalize(part)) + .filter(Boolean); + if (learningAlts.includes(itemLearning) && referenceAlts.includes(itemReference)) { + toDelete.push(key); + } + }); + if (!toDelete.length) return; + + const next = { ...this.hardVocabMap }; + const nextMastery = { ...this.hardMasteryByKey }; + toDelete.forEach((key) => { + delete next[key]; + delete nextMastery[key]; + }); + this.hardVocabMap = next; + this.hardMasteryByKey = nextMastery; + }, markCurrentAsHard() { if (!this.current) return; + this.removeHardEntriesForItem(this.current); const key = this.getHardKey(this.current); this.hardVocabMap = { ...this.hardVocabMap, @@ -362,16 +417,9 @@ export default { }, unmarkCurrentAsHard() { if (!this.current) return; - const key = this.getHardKey(this.current); - if (!this.hardVocabMap[key]) return; - const next = { ...this.hardVocabMap }; - delete next[key]; - this.hardVocabMap = next; - if (this.hardMasteryByKey[key] != null) { - const nextMastery = { ...this.hardMasteryByKey }; - delete nextMastery[key]; - this.hardMasteryByKey = nextMastery; - } + const matchingKey = this.findMatchingHardKey(this.current); + if (!matchingKey) return; + this.removeHardEntriesForItem(this.current); if (this.hardPhaseActive && this.hardRemainingCount <= 0) { this.hardPhaseActive = false; } @@ -986,9 +1034,10 @@ export default { if (this.hardPhaseActive && this.current) { const key = this.getHardKey(this.current); this.hardMasteryByKey[key] = Math.max(0, Number(this.hardMasteryByKey[key]) || 0) + 1; - if ((Number(this.hardMasteryByKey[key]) || 0) >= HARD_REQUIRED_CONSECUTIVE_CORRECT && this.hardVocabMap[key]) { + const mapKey = this.findMatchingHardKey(this.current); + if ((Number(this.hardMasteryByKey[key]) || 0) >= HARD_REQUIRED_CONSECUTIVE_CORRECT && mapKey) { const next = { ...this.hardVocabMap }; - delete next[key]; + delete next[mapKey]; this.hardVocabMap = next; this.saveHardVocabMap(); } diff --git a/frontend/src/views/social/VocabCourseView.vue b/frontend/src/views/social/VocabCourseView.vue index 64e3c3a..5e3dbc5 100644 --- a/frontend/src/views/social/VocabCourseView.vue +++ b/frontend/src/views/social/VocabCourseView.vue @@ -29,6 +29,34 @@ +
+
+

{{ $t('socialnetwork.vocab.courses.startHardVocabTrainer', { count: hardVocabCount }) }}

+ +
+
+
+
+ {{ entry.learning }} + {{ entry.reference }} +
+ +
+
+
+
{{ $t('socialnetwork.vocab.courses.languageAssistantEyebrow') }} @@ -550,14 +578,15 @@ export default { try { const raw = localStorage.getItem(key); const parsed = raw ? JSON.parse(raw) : {}; - const values = parsed && typeof parsed === 'object' ? Object.values(parsed) : []; + const values = parsed && typeof parsed === 'object' ? Object.entries(parsed) : []; this.hardVocabList = values - .map((entry, idx) => { + .map(([entryKey, entry], idx) => { const learning = String(entry?.learning || '').trim(); const reference = String(entry?.reference || '').trim(); if (!learning || !reference) return null; return { id: `hard-${idx}-${learning}-${reference}`, + key: String(entryKey || ''), learning, reference }; @@ -567,6 +596,21 @@ export default { this.hardVocabList = []; } }, + removeHardVocabEntry(entryKey) { + const key = this.hardStorageKey(); + if (!key || !entryKey) return; + try { + const raw = localStorage.getItem(key); + const parsed = raw ? JSON.parse(raw) : {}; + if (!parsed || typeof parsed !== 'object' || !parsed[entryKey]) return; + const next = { ...parsed }; + delete next[entryKey]; + localStorage.setItem(key, JSON.stringify(next)); + this.refreshHardVocabList(); + } catch (_) { + // ignore storage parse/write errors + } + }, handleWindowFocus() { this.refreshHardVocabList(); }, @@ -1040,6 +1084,48 @@ export default { flex-shrink: 0; } +.course-hard-list { + padding: 16px 18px; +} + +.course-hard-list__header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 12px; +} + +.course-hard-list__header h3 { + margin: 0; + font-size: 1rem; +} + +.course-hard-list__items { + display: grid; + gap: 8px; +} + +.course-hard-list__item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding: 10px 12px; + border: 1px solid var(--color-border, #e1dbd4); + border-radius: 10px; + background: rgba(255, 255, 255, 0.72); +} + +.course-hard-list__texts { + display: grid; + gap: 2px; +} + +.course-hard-list__texts span { + color: var(--color-text-secondary, #6b625b); +} + .course-assistant { display: flex; align-items: flex-start;