feat(VocabPracticeDialog, VocabCourseView): enhance hard vocabulary management and UI
All checks were successful
Deploy to production / deploy (push) Successful in 2m6s
All checks were successful
Deploy to production / deploy (push) Successful in 2m6s
- 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.
This commit is contained in:
@@ -297,11 +297,11 @@ export default {
|
|||||||
},
|
},
|
||||||
isCurrentMarkedHard() {
|
isCurrentMarkedHard() {
|
||||||
if (!this.current) return false;
|
if (!this.current) return false;
|
||||||
return Boolean(this.hardVocabMap[this.getHardKey(this.current)]);
|
return this.isItemMarkedHard(this.current);
|
||||||
},
|
},
|
||||||
hardPoolItems() {
|
hardPoolItems() {
|
||||||
if (!Array.isArray(this.pool) || this.pool.length === 0) return [];
|
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() {
|
hardRemainingCount() {
|
||||||
return this.hardPoolItems.filter((item) => {
|
return this.hardPoolItems.filter((item) => {
|
||||||
@@ -344,8 +344,63 @@ export default {
|
|||||||
const reference = this.normalize(item?.reference || '');
|
const reference = this.normalize(item?.reference || '');
|
||||||
return `${learning}|${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() {
|
markCurrentAsHard() {
|
||||||
if (!this.current) return;
|
if (!this.current) return;
|
||||||
|
this.removeHardEntriesForItem(this.current);
|
||||||
const key = this.getHardKey(this.current);
|
const key = this.getHardKey(this.current);
|
||||||
this.hardVocabMap = {
|
this.hardVocabMap = {
|
||||||
...this.hardVocabMap,
|
...this.hardVocabMap,
|
||||||
@@ -362,16 +417,9 @@ export default {
|
|||||||
},
|
},
|
||||||
unmarkCurrentAsHard() {
|
unmarkCurrentAsHard() {
|
||||||
if (!this.current) return;
|
if (!this.current) return;
|
||||||
const key = this.getHardKey(this.current);
|
const matchingKey = this.findMatchingHardKey(this.current);
|
||||||
if (!this.hardVocabMap[key]) return;
|
if (!matchingKey) return;
|
||||||
const next = { ...this.hardVocabMap };
|
this.removeHardEntriesForItem(this.current);
|
||||||
delete next[key];
|
|
||||||
this.hardVocabMap = next;
|
|
||||||
if (this.hardMasteryByKey[key] != null) {
|
|
||||||
const nextMastery = { ...this.hardMasteryByKey };
|
|
||||||
delete nextMastery[key];
|
|
||||||
this.hardMasteryByKey = nextMastery;
|
|
||||||
}
|
|
||||||
if (this.hardPhaseActive && this.hardRemainingCount <= 0) {
|
if (this.hardPhaseActive && this.hardRemainingCount <= 0) {
|
||||||
this.hardPhaseActive = false;
|
this.hardPhaseActive = false;
|
||||||
}
|
}
|
||||||
@@ -986,9 +1034,10 @@ export default {
|
|||||||
if (this.hardPhaseActive && this.current) {
|
if (this.hardPhaseActive && this.current) {
|
||||||
const key = this.getHardKey(this.current);
|
const key = this.getHardKey(this.current);
|
||||||
this.hardMasteryByKey[key] = Math.max(0, Number(this.hardMasteryByKey[key]) || 0) + 1;
|
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 };
|
const next = { ...this.hardVocabMap };
|
||||||
delete next[key];
|
delete next[mapKey];
|
||||||
this.hardVocabMap = next;
|
this.hardVocabMap = next;
|
||||||
this.saveHardVocabMap();
|
this.saveHardVocabMap();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,34 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<section v-if="hardVocabCount > 0" class="surface-card course-hard-list">
|
||||||
|
<div class="course-hard-list__header">
|
||||||
|
<h3>{{ $t('socialnetwork.vocab.courses.startHardVocabTrainer', { count: hardVocabCount }) }}</h3>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button-secondary"
|
||||||
|
@click="openHardPractice"
|
||||||
|
>
|
||||||
|
{{ $t('socialnetwork.vocab.courses.practiceInTrainer') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="course-hard-list__items">
|
||||||
|
<div v-for="entry in hardVocabList" :key="entry.key" class="course-hard-list__item">
|
||||||
|
<div class="course-hard-list__texts">
|
||||||
|
<strong>{{ entry.learning }}</strong>
|
||||||
|
<span>{{ entry.reference }}</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-delete"
|
||||||
|
@click="removeHardVocabEntry(entry.key)"
|
||||||
|
>
|
||||||
|
{{ $t('socialnetwork.vocab.courses.unmarkVocabHard') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="surface-card course-assistant">
|
<section class="surface-card course-assistant">
|
||||||
<div>
|
<div>
|
||||||
<span class="course-assistant__eyebrow">{{ $t('socialnetwork.vocab.courses.languageAssistantEyebrow') }}</span>
|
<span class="course-assistant__eyebrow">{{ $t('socialnetwork.vocab.courses.languageAssistantEyebrow') }}</span>
|
||||||
@@ -550,14 +578,15 @@ export default {
|
|||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(key);
|
const raw = localStorage.getItem(key);
|
||||||
const parsed = raw ? JSON.parse(raw) : {};
|
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
|
this.hardVocabList = values
|
||||||
.map((entry, idx) => {
|
.map(([entryKey, entry], idx) => {
|
||||||
const learning = String(entry?.learning || '').trim();
|
const learning = String(entry?.learning || '').trim();
|
||||||
const reference = String(entry?.reference || '').trim();
|
const reference = String(entry?.reference || '').trim();
|
||||||
if (!learning || !reference) return null;
|
if (!learning || !reference) return null;
|
||||||
return {
|
return {
|
||||||
id: `hard-${idx}-${learning}-${reference}`,
|
id: `hard-${idx}-${learning}-${reference}`,
|
||||||
|
key: String(entryKey || ''),
|
||||||
learning,
|
learning,
|
||||||
reference
|
reference
|
||||||
};
|
};
|
||||||
@@ -567,6 +596,21 @@ export default {
|
|||||||
this.hardVocabList = [];
|
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() {
|
handleWindowFocus() {
|
||||||
this.refreshHardVocabList();
|
this.refreshHardVocabList();
|
||||||
},
|
},
|
||||||
@@ -1040,6 +1084,48 @@ export default {
|
|||||||
flex-shrink: 0;
|
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 {
|
.course-assistant {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
|||||||
Reference in New Issue
Block a user