feat(vocab): add intensive review status dialog and related functionality
This commit is contained in:
@@ -774,6 +774,12 @@
|
||||
"courseFlowIntensiveTitle": "Fällige Intensivphase",
|
||||
"courseFlowIntensiveDescription": "Verdichtete Wiederholung, sobald der Block davor weitgehend sitzt.",
|
||||
"courseFlowIntensiveEmpty": "Aktuell ist keine neue Intensivphase freigeschaltet.",
|
||||
"courseFlowIntensiveStatusAction": "Freischaltung prüfen",
|
||||
"courseFlowIntensiveStatusTitle": "Status der Intensivphase",
|
||||
"courseFlowIntensiveStatusAllDone": "Alle Intensivphasen dieses Kurses sind bereits abgeschlossen.",
|
||||
"courseFlowIntensiveStatusReady": "„{lesson}“ ist bereit und kann jetzt geöffnet werden.",
|
||||
"courseFlowIntensiveStatusProgress": "Für „{lesson}“ sind {done} von {total} vorausgehenden Lektionen im selben Block abgeschlossen.",
|
||||
"courseFlowIntensiveStatusNoDate": "Die Intensivphase wird nicht nach einer Wartezeit freigeschaltet, sondern sofort nach Abschluss dieser Lektionen.",
|
||||
"courseFlowPracticeTitle": "Freie Vertiefung",
|
||||
"courseFlowPracticeDescription": "Abgeschlossene Lektionen für lockeres Nachtrainieren außerhalb des Pflichtpfads.",
|
||||
"courseFlowPracticeEmpty": "Sobald du erste Lektionen abgeschlossen hast, erscheinen sie hier für freies Nachtrainieren.",
|
||||
|
||||
@@ -774,6 +774,12 @@
|
||||
"courseFlowIntensiveTitle": "Due intensive review",
|
||||
"courseFlowIntensiveDescription": "Condensed review once the block before it is mostly stable.",
|
||||
"courseFlowIntensiveEmpty": "No new intensive review is unlocked right now.",
|
||||
"courseFlowIntensiveStatusAction": "Check availability",
|
||||
"courseFlowIntensiveStatusTitle": "Intensive review status",
|
||||
"courseFlowIntensiveStatusAllDone": "All intensive review phases in this course are already complete.",
|
||||
"courseFlowIntensiveStatusReady": "“{lesson}” is ready and can be opened now.",
|
||||
"courseFlowIntensiveStatusProgress": "For “{lesson}”, {done} of {total} preceding lessons in the same block are complete.",
|
||||
"courseFlowIntensiveStatusNoDate": "The intensive review unlocks immediately after these lessons are complete; it does not have a waiting period.",
|
||||
"courseFlowPracticeTitle": "Free practice",
|
||||
"courseFlowPracticeDescription": "Completed lessons for relaxed extra practice outside the required path.",
|
||||
"courseFlowPracticeEmpty": "As soon as you complete your first lessons, they will appear here for free practice.",
|
||||
|
||||
@@ -206,7 +206,12 @@
|
||||
<span>{{ $t('socialnetwork.vocab.courses.lessonBlockLabel', { number: nextIntensiveReviewLesson.pedagogy?.blockNumber || '—' }) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<p v-else class="course-flow-card__empty">{{ $t('socialnetwork.vocab.courses.courseFlowIntensiveEmpty') }}</p>
|
||||
<template v-else>
|
||||
<p class="course-flow-card__empty">{{ $t('socialnetwork.vocab.courses.courseFlowIntensiveEmpty') }}</p>
|
||||
<button type="button" class="course-flow-card__status" @click="showIntensiveStatusDialog = true">
|
||||
{{ $t('socialnetwork.vocab.courses.courseFlowIntensiveStatusAction') }}
|
||||
</button>
|
||||
</template>
|
||||
</article>
|
||||
|
||||
<article class="course-flow-card">
|
||||
@@ -312,6 +317,37 @@
|
||||
|
||||
<VocabPracticeDialog ref="practiceDialog" />
|
||||
|
||||
<div v-if="showIntensiveStatusDialog" class="dialog-overlay" @click="showIntensiveStatusDialog = false">
|
||||
<section class="dialog intensive-status-dialog" role="dialog" aria-modal="true" @click.stop>
|
||||
<h3>{{ $t('socialnetwork.vocab.courses.courseFlowIntensiveStatusTitle') }}</h3>
|
||||
<template v-if="intensiveReviewStatus.allCompleted">
|
||||
<p>{{ $t('socialnetwork.vocab.courses.courseFlowIntensiveStatusAllDone') }}</p>
|
||||
</template>
|
||||
<template v-else-if="intensiveReviewStatus.ready">
|
||||
<p>{{ $t('socialnetwork.vocab.courses.courseFlowIntensiveStatusReady', { lesson: intensiveReviewStatus.lesson.title }) }}</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p>{{ $t('socialnetwork.vocab.courses.courseFlowIntensiveStatusProgress', {
|
||||
done: intensiveReviewStatus.completedPrerequisiteCount,
|
||||
total: intensiveReviewStatus.prerequisiteLessons.length,
|
||||
lesson: intensiveReviewStatus.lesson.title
|
||||
}) }}</p>
|
||||
<p class="intensive-status-dialog__hint">{{ $t('socialnetwork.vocab.courses.courseFlowIntensiveStatusNoDate') }}</p>
|
||||
<ul class="intensive-status-dialog__list">
|
||||
<li v-for="lesson in intensiveReviewStatus.missingPrerequisiteLessons" :key="lesson.id">
|
||||
<span><strong>#{{ lesson.lessonNumber }}</strong> {{ lesson.title }}</span>
|
||||
<button type="button" class="button-secondary" @click="openIntensivePrerequisite(lesson.id)">
|
||||
{{ $t('socialnetwork.vocab.courses.courseTodayPlanOpen') }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
<div class="form-actions">
|
||||
<button type="button" class="button-secondary" @click="showIntensiveStatusDialog = false">{{ $t('general.close') }}</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Add Lesson Dialog -->
|
||||
<div v-if="showAddLessonDialog" class="dialog-overlay" @click="showAddLessonDialog = false">
|
||||
<div class="dialog" @click.stop>
|
||||
@@ -376,6 +412,7 @@ export default {
|
||||
srsDailyLimit: 50,
|
||||
srsTodayRemaining: null,
|
||||
srsLoading: false,
|
||||
showIntensiveStatusDialog: false,
|
||||
showAddLessonDialog: false,
|
||||
assistantSettings: null,
|
||||
hardVocabList: [],
|
||||
@@ -460,20 +497,46 @@ export default {
|
||||
},
|
||||
nextIntensiveReviewLesson() {
|
||||
return this.sortedLessons.find((lesson) => {
|
||||
const isIntensive = lesson.pedagogy?.didacticMode === 'intensive_review' || lesson.pedagogy?.isIntensiveReview;
|
||||
const isIntensive = this.isIntensiveReviewLesson(lesson);
|
||||
if (!isIntensive) return false;
|
||||
if (this.getLessonProgress(lesson.id, lesson)?.completed) return false;
|
||||
|
||||
const blockNumber = lesson.pedagogy?.blockNumber;
|
||||
const blockLessons = this.sortedLessons.filter((candidate) => {
|
||||
if ((candidate.pedagogy?.blockNumber || null) !== blockNumber) return false;
|
||||
const candidateIsIntensive = candidate.pedagogy?.didacticMode === 'intensive_review' || candidate.pedagogy?.isIntensiveReview;
|
||||
const candidateIsIntensive = this.isIntensiveReviewLesson(candidate);
|
||||
return !candidateIsIntensive && candidate.lessonNumber < lesson.lessonNumber;
|
||||
});
|
||||
|
||||
return blockLessons.length > 0 && blockLessons.every((candidate) => this.getLessonProgress(candidate.id, candidate)?.completed);
|
||||
}) || null;
|
||||
},
|
||||
intensiveReviewStatus() {
|
||||
const lesson = this.sortedLessons.find((candidate) => (
|
||||
this.isIntensiveReviewLesson(candidate)
|
||||
&& !this.getLessonProgress(candidate.id, candidate)?.completed
|
||||
));
|
||||
if (!lesson) {
|
||||
return { allCompleted: true, ready: false, prerequisiteLessons: [], missingPrerequisiteLessons: [], completedPrerequisiteCount: 0 };
|
||||
}
|
||||
|
||||
const prerequisiteLessons = this.sortedLessons.filter((candidate) => (
|
||||
!this.isIntensiveReviewLesson(candidate)
|
||||
&& candidate.pedagogy?.blockNumber === lesson.pedagogy?.blockNumber
|
||||
&& candidate.lessonNumber < lesson.lessonNumber
|
||||
));
|
||||
const missingPrerequisiteLessons = prerequisiteLessons.filter((candidate) => (
|
||||
!this.getLessonProgress(candidate.id, candidate)?.completed
|
||||
));
|
||||
return {
|
||||
allCompleted: false,
|
||||
lesson,
|
||||
ready: prerequisiteLessons.length > 0 && missingPrerequisiteLessons.length === 0,
|
||||
prerequisiteLessons,
|
||||
missingPrerequisiteLessons,
|
||||
completedPrerequisiteCount: prerequisiteLessons.length - missingPrerequisiteLessons.length
|
||||
};
|
||||
},
|
||||
freePracticeLessons() {
|
||||
return this.sortedLessons
|
||||
.filter((lesson) => {
|
||||
@@ -566,6 +629,9 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isIntensiveReviewLesson(lesson) {
|
||||
return lesson?.pedagogy?.didacticMode === 'intensive_review' || Boolean(lesson?.pedagogy?.isIntensiveReview);
|
||||
},
|
||||
displayCourseTitle(course) {
|
||||
return localizeVocabCourseTitle(course?.title, this.$i18n?.locale) || '';
|
||||
},
|
||||
@@ -935,6 +1001,10 @@ export default {
|
||||
openLesson(lessonId) {
|
||||
this.$router.push(`/socialnetwork/vocab/courses/${this.courseId}/lessons/${lessonId}`);
|
||||
},
|
||||
openIntensivePrerequisite(lessonId) {
|
||||
this.showIntensiveStatusDialog = false;
|
||||
this.openLesson(lessonId);
|
||||
},
|
||||
todayPlanStepLabel(type) {
|
||||
const key = {
|
||||
review_due: 'courseTodayPlanStepReviewDue',
|
||||
@@ -1382,6 +1452,40 @@ export default {
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.course-flow-card__status {
|
||||
margin-top: 10px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #2f6b3d;
|
||||
font-weight: 650;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.intensive-status-dialog__hint {
|
||||
color: var(--color-text-secondary);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.intensive-status-dialog__list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin: 16px 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.intensive-status-dialog__list li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.share-code {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user