feat(i18n, frontend): enhance course planning with optional steps and localization updates
All checks were successful
Deploy to production / deploy (push) Successful in 2m52s
All checks were successful
Deploy to production / deploy (push) Successful in 2m52s
- Added a new section for optional learning steps in the course planning UI, allowing users to engage with additional content when no mandatory tasks are due. - Updated localization files for Cebuano, German, English, Spanish, and French to reflect changes in course planning instructions and titles, ensuring clarity and consistency across languages. - Improved pedagogical logic for lesson recommendations, focusing on cognitive load and spaced repetition principles to enhance user learning experience.
This commit is contained in:
@@ -74,6 +74,26 @@
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div v-else-if="showTodayPlanSoftOptional" class="course-today-plan course-today-plan--soft">
|
||||
<h4 class="course-today-plan__title">{{ $t('socialnetwork.vocab.courses.courseTodayPlanSoftTitle') }}</h4>
|
||||
<p class="course-today-plan__intro">{{ $t('socialnetwork.vocab.courses.courseTodayPlanSoftIntro') }}</p>
|
||||
<ol class="course-today-plan__list">
|
||||
<li
|
||||
v-for="(step, idx) in todayPlanOptionalSteps"
|
||||
:key="`opt-${step.type}-${step.lesson.id}-${idx}`"
|
||||
class="course-today-plan__item"
|
||||
>
|
||||
<div class="course-today-plan__item-main">
|
||||
<span class="course-today-plan__step-label">{{ todayPlanStepLabel(step.type) }}</span>
|
||||
<span class="course-today-plan__lesson-title">{{ step.lesson.title }}</span>
|
||||
<span class="course-today-plan__lesson-meta">#{{ step.lesson.lessonNumber }}</span>
|
||||
</div>
|
||||
<button type="button" class="course-today-plan__action course-today-plan__action--soft" @click="openTodayPlanStep(step)">
|
||||
{{ step.type === 'practice' ? $t('socialnetwork.vocab.courses.courseTodayPlanTrainer') : $t('socialnetwork.vocab.courses.courseTodayPlanOpen') }}
|
||||
</button>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div v-else class="course-today-plan course-today-plan--empty">
|
||||
<h4 class="course-today-plan__title">{{ $t('socialnetwork.vocab.courses.courseTodayPlanTitle') }}</h4>
|
||||
<p class="course-today-plan__intro">{{ $t('socialnetwork.vocab.courses.courseTodayPlanEmpty') }}</p>
|
||||
@@ -400,6 +420,15 @@ export default {
|
||||
})
|
||||
.slice(0, 4);
|
||||
},
|
||||
/**
|
||||
* Didaktischer Tagesvorschlag (nicht Kalender-fixiert, aber nach Prinzipien):
|
||||
* 1) Spacing: fällige Kurz-Wiederholungen zuerst (Abruf vor neuem Input).
|
||||
* 2) Kognitive Last: nur eine begrenzte „Einheit“ neuer Block-Lektionen pro Aufruf,
|
||||
* schwere/checkpoint/intensive Lektionen zählen stärker (siehe pickPedagogicalBlockSteps).
|
||||
* 3) Dann Intensivphase, wenn freigeschaltet.
|
||||
* Keine „Fallbacks“ in dieser Liste: weitere Lektion/Trainer nur unter
|
||||
* todayPlanOptionalSteps mit eigener Einleitung (Pause/Spacing sinnvoller als Pflicht).
|
||||
*/
|
||||
todayRecommendedSteps() {
|
||||
const out = [];
|
||||
const seen = new Set();
|
||||
@@ -411,20 +440,36 @@ export default {
|
||||
out.push({ type, lesson });
|
||||
};
|
||||
this.dueReviewLessons.forEach((l) => push('review_due', l));
|
||||
this.currentBlockLessons.slice(0, 3).forEach((l) => push('block', l));
|
||||
this.pickPedagogicalBlockSteps().forEach((l) => push('block', l));
|
||||
if (this.nextIntensiveReviewLesson) {
|
||||
push('intensive', this.nextIntensiveReviewLesson);
|
||||
}
|
||||
if (out.length === 0 && this.currentLesson) {
|
||||
return out.slice(0, 8);
|
||||
},
|
||||
/** Optional: nur wenn kein didaktischer Pflichtplan – semantisch „könntest du, Pause oft besser“. */
|
||||
todayPlanOptionalSteps() {
|
||||
const out = [];
|
||||
const seen = new Set();
|
||||
const push = (type, lesson) => {
|
||||
if (!lesson?.id || seen.has(lesson.id)) {
|
||||
return;
|
||||
}
|
||||
seen.add(lesson.id);
|
||||
out.push({ type, lesson });
|
||||
};
|
||||
if (this.currentLesson) {
|
||||
const p = this.getLessonProgress(this.currentLesson.id, this.currentLesson);
|
||||
if (!p?.completed) {
|
||||
push('continue', this.currentLesson);
|
||||
}
|
||||
}
|
||||
if (out.length === 0 && this.freePracticeLessons.length > 0) {
|
||||
if (this.freePracticeLessons.length > 0) {
|
||||
push('practice', this.freePracticeLessons[0]);
|
||||
}
|
||||
return out.slice(0, 8);
|
||||
return out;
|
||||
},
|
||||
showTodayPlanSoftOptional() {
|
||||
return this.todayRecommendedSteps.length === 0 && this.todayPlanOptionalSteps.length > 0;
|
||||
},
|
||||
isLessonNumberValid() {
|
||||
return Number(this.newLesson.lessonNumber) > 0;
|
||||
@@ -510,6 +555,45 @@ export default {
|
||||
const progress = this.getLessonProgress(lessonId, lesson);
|
||||
return progress?.lastAccessedAt || progress?.completedAt || progress?.updatedAt || '';
|
||||
},
|
||||
/**
|
||||
* Grobe „Last-Einheit“ pro Lektion für Tageskappung (1 = leicht, 2 = schwer/Checkpoint/Intensiv im Block).
|
||||
*/
|
||||
blockLessonDailyLoadUnits(lesson) {
|
||||
if (!lesson) {
|
||||
return 1;
|
||||
}
|
||||
const p = lesson.pedagogy || {};
|
||||
const mode = p.didacticMode;
|
||||
if (mode === 'checkpoint' || mode === 'intensive_review' || p.isIntensiveReview) {
|
||||
return 2;
|
||||
}
|
||||
const w = Number(p.difficultyWeight);
|
||||
if (Number.isFinite(w) && w >= 4) {
|
||||
return 2;
|
||||
}
|
||||
return 1;
|
||||
},
|
||||
/**
|
||||
* Offene Lektionen im Block nach Nummer, mit Obergrenze für „neues Material“ pro Empfehlungsliste
|
||||
* (Spacing + cognitive load: nicht drei schwere Lektionen auf einmal).
|
||||
*/
|
||||
pickPedagogicalBlockSteps() {
|
||||
const list = [...this.currentBlockLessons].sort(
|
||||
(a, b) => Number(a.lessonNumber) - Number(b.lessonNumber)
|
||||
);
|
||||
const MAX_UNITS = 3;
|
||||
const picked = [];
|
||||
let units = 0;
|
||||
for (const l of list) {
|
||||
const cost = this.blockLessonDailyLoadUnits(l);
|
||||
if (units + cost > MAX_UNITS) {
|
||||
break;
|
||||
}
|
||||
picked.push(l);
|
||||
units += cost;
|
||||
}
|
||||
return picked;
|
||||
},
|
||||
daysSince(dateString) {
|
||||
if (!dateString) {
|
||||
return 0;
|
||||
@@ -1360,6 +1444,11 @@ export default {
|
||||
border: 1px solid rgba(212, 184, 150, 0.45);
|
||||
}
|
||||
|
||||
.course-today-plan--soft {
|
||||
border-style: dashed;
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.course-today-plan--empty {
|
||||
background: rgba(250, 250, 250, 0.85);
|
||||
border-color: var(--color-border, #e0dcd6);
|
||||
@@ -1433,6 +1522,10 @@ export default {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.course-today-plan__action--soft {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.course-today-plan__action:hover {
|
||||
border-color: #b8a896;
|
||||
background: #fff;
|
||||
|
||||
Reference in New Issue
Block a user