feat(vocab): implement scheduled review management in VocabService and UI updates
All checks were successful
Deploy to production / deploy (push) Successful in 2m56s
All checks were successful
Deploy to production / deploy (push) Successful in 2m56s
- Added methods in VocabService to handle scheduled review states, including normalization of review dates and management of review stages. - Enhanced lesson state management to support review scheduling, improving the learning process for users. - Updated VocabCourseView and VocabLessonView to display review statuses and due dates, providing clearer feedback on lesson progress and review requirements. - Introduced new UI elements to indicate review status, enhancing user engagement and understanding of lesson timelines.
This commit is contained in:
@@ -38,6 +38,112 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="course.lessons && course.lessons.length > 0" class="surface-card course-flow">
|
||||
<div class="course-flow__header">
|
||||
<div>
|
||||
<span class="course-flow__eyebrow">Tagesfluss</span>
|
||||
<h3>Heute sinnvoll weitermachen</h3>
|
||||
<p>Die Reihenfolge folgt dem Konzept: fällige Wiederholung zuerst, dann aktueller Block, danach Intensivphase und freie Vertiefung.</p>
|
||||
</div>
|
||||
<div class="course-flow__stats">
|
||||
<span class="course-flow__stat">Fällige Wiederholung: {{ dueReviewLessons.length }}</span>
|
||||
<span class="course-flow__stat">Aktiver Block: {{ currentBlockNumber || '—' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="course-flow__grid">
|
||||
<article class="course-flow-card">
|
||||
<div class="course-flow-card__top">
|
||||
<span class="course-flow-card__badge course-flow-card__badge--review">1</span>
|
||||
<div>
|
||||
<h4>Fällige Wiederholung</h4>
|
||||
<p>Bereits abgeschlossene Lektionen, die heute wieder drankommen sollten.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="dueReviewLessons.length > 0" class="course-flow-card__list">
|
||||
<button
|
||||
v-for="lesson in dueReviewLessons"
|
||||
:key="`due-${lesson.id}`"
|
||||
type="button"
|
||||
class="course-flow-lesson"
|
||||
@click="openLesson(lesson.id)"
|
||||
>
|
||||
<strong>{{ lesson.title }}</strong>
|
||||
<span>{{ formatReviewDue(getLessonProgress(lesson.id)?.reviewNextDueAt) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<p v-else class="course-flow-card__empty">Heute ist keine ältere Lektion als fällige Wiederholung markiert.</p>
|
||||
</article>
|
||||
|
||||
<article class="course-flow-card">
|
||||
<div class="course-flow-card__top">
|
||||
<span class="course-flow-card__badge course-flow-card__badge--block">2</span>
|
||||
<div>
|
||||
<h4>Aktueller Block</h4>
|
||||
<p>Hier liegt der nächste reguläre Fortschritt im Kurs.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="currentBlockLessons.length > 0" class="course-flow-card__list">
|
||||
<button
|
||||
v-for="lesson in currentBlockLessons"
|
||||
:key="`block-${lesson.id}`"
|
||||
type="button"
|
||||
class="course-flow-lesson"
|
||||
@click="openLesson(lesson.id)"
|
||||
>
|
||||
<strong>{{ lesson.title }}</strong>
|
||||
<span>#{{ lesson.lessonNumber }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<p v-else class="course-flow-card__empty">Der aktuelle Block ist bereits abgeschlossen oder es gibt gerade keine offene Blocklektion.</p>
|
||||
</article>
|
||||
|
||||
<article class="course-flow-card">
|
||||
<div class="course-flow-card__top">
|
||||
<span class="course-flow-card__badge course-flow-card__badge--intensive">3</span>
|
||||
<div>
|
||||
<h4>Fällige Intensivphase</h4>
|
||||
<p>Verdichtete Wiederholung, sobald der Block davor weitgehend sitzt.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="nextIntensiveReviewLesson" class="course-flow-card__list">
|
||||
<button
|
||||
type="button"
|
||||
class="course-flow-lesson"
|
||||
@click="openLesson(nextIntensiveReviewLesson.id)"
|
||||
>
|
||||
<strong>{{ nextIntensiveReviewLesson.title }}</strong>
|
||||
<span>Block {{ nextIntensiveReviewLesson.pedagogy?.blockNumber || '—' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<p v-else class="course-flow-card__empty">Aktuell ist keine neue Intensivphase freigeschaltet.</p>
|
||||
</article>
|
||||
|
||||
<article class="course-flow-card">
|
||||
<div class="course-flow-card__top">
|
||||
<span class="course-flow-card__badge course-flow-card__badge--practice">4</span>
|
||||
<div>
|
||||
<h4>Freie Vertiefung</h4>
|
||||
<p>Abgeschlossene Lektionen für lockeres Nachtrainieren außerhalb des Pflichtpfads.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="freePracticeLessons.length > 0" class="course-flow-card__list">
|
||||
<button
|
||||
v-for="lesson in freePracticeLessons"
|
||||
:key="`practice-${lesson.id}`"
|
||||
type="button"
|
||||
class="course-flow-lesson"
|
||||
@click="openLessonPractice(lesson)"
|
||||
>
|
||||
<strong>{{ lesson.title }}</strong>
|
||||
<span>Im Trainer üben</span>
|
||||
</button>
|
||||
</div>
|
||||
<p v-else class="course-flow-card__empty">Sobald du erste Lektionen abgeschlossen hast, erscheinen sie hier für freies Nachtrainieren.</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div v-if="isOwner" class="owner-actions">
|
||||
<button @click="showAddLessonDialog = true">{{ $t('socialnetwork.vocab.courses.addLesson') }}</button>
|
||||
<button @click="editCourse">{{ $t('socialnetwork.vocab.courses.edit') }}</button>
|
||||
@@ -54,7 +160,7 @@
|
||||
<span class="lessons-count">{{ course.lessons.length }} Lektionen</span>
|
||||
</div>
|
||||
<div class="lesson-cards">
|
||||
<article v-for="lesson in course.lessons" :key="lesson.id" class="lesson-card">
|
||||
<article v-for="lesson in sortedLessons" :key="lesson.id" class="lesson-card">
|
||||
<div class="lesson-card__header">
|
||||
<span class="lesson-number">#{{ lesson.lessonNumber }}</span>
|
||||
<div class="lesson-status-content">
|
||||
@@ -67,6 +173,13 @@
|
||||
<span v-else class="status-new">
|
||||
{{ $t('socialnetwork.vocab.courses.notStarted') }}
|
||||
</span>
|
||||
<span
|
||||
v-if="getReviewBadgeLabel(getLessonProgress(lesson.id))"
|
||||
class="review-badge"
|
||||
:class="getReviewBadgeClass(getLessonProgress(lesson.id))"
|
||||
>
|
||||
{{ getReviewBadgeLabel(getLessonProgress(lesson.id)) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lesson-title-content">
|
||||
@@ -180,24 +293,84 @@ export default {
|
||||
isOwner() {
|
||||
return this.course && this.course.ownerUserId === this.user?.id;
|
||||
},
|
||||
sortedLessons() {
|
||||
if (!this.course?.lessons) {
|
||||
return [];
|
||||
}
|
||||
return [...this.course.lessons].sort((a, b) => a.lessonNumber - b.lessonNumber);
|
||||
},
|
||||
currentLesson() {
|
||||
if (!this.course || !this.course.lessons || this.course.lessons.length === 0) {
|
||||
if (this.sortedLessons.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Sortiere Lektionen nach lessonNumber
|
||||
const sortedLessons = [...this.course.lessons].sort((a, b) => a.lessonNumber - b.lessonNumber);
|
||||
|
||||
|
||||
// Finde die erste nicht abgeschlossene Lektion
|
||||
for (const lesson of sortedLessons) {
|
||||
for (const lesson of this.sortedLessons) {
|
||||
const progress = this.getLessonProgress(lesson.id);
|
||||
if (!progress || !progress.completed) {
|
||||
return lesson;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Alle Lektionen abgeschlossen - zeige die letzte Lektion
|
||||
return sortedLessons[sortedLessons.length - 1];
|
||||
return this.sortedLessons[this.sortedLessons.length - 1];
|
||||
},
|
||||
currentBlockNumber() {
|
||||
return this.currentLesson?.pedagogy?.blockNumber || null;
|
||||
},
|
||||
dueReviewLessons() {
|
||||
return this.sortedLessons
|
||||
.filter((lesson) => {
|
||||
const progress = this.getLessonProgress(lesson.id);
|
||||
return Boolean(progress?.completed && progress?.reviewDue);
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const left = this.getLessonProgress(a.id)?.reviewNextDueAt || '';
|
||||
const right = this.getLessonProgress(b.id)?.reviewNextDueAt || '';
|
||||
return left.localeCompare(right);
|
||||
})
|
||||
.slice(0, 4);
|
||||
},
|
||||
currentBlockLessons() {
|
||||
if (!this.currentBlockNumber) {
|
||||
return [];
|
||||
}
|
||||
return this.sortedLessons.filter((lesson) => {
|
||||
const lessonBlock = lesson.pedagogy?.blockNumber;
|
||||
if (lessonBlock !== this.currentBlockNumber) {
|
||||
return false;
|
||||
}
|
||||
return !this.getLessonProgress(lesson.id)?.completed;
|
||||
});
|
||||
},
|
||||
nextIntensiveReviewLesson() {
|
||||
return this.sortedLessons.find((lesson) => {
|
||||
const isIntensive = lesson.pedagogy?.didacticMode === 'intensive_review' || lesson.pedagogy?.isIntensiveReview;
|
||||
if (!isIntensive) return false;
|
||||
if (this.getLessonProgress(lesson.id)?.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;
|
||||
return !candidateIsIntensive && candidate.lessonNumber < lesson.lessonNumber;
|
||||
});
|
||||
|
||||
return blockLessons.length > 0 && blockLessons.every((candidate) => this.getLessonProgress(candidate.id)?.completed);
|
||||
}) || null;
|
||||
},
|
||||
freePracticeLessons() {
|
||||
return this.sortedLessons
|
||||
.filter((lesson) => {
|
||||
const progress = this.getLessonProgress(lesson.id);
|
||||
return Boolean(progress?.completed) && !progress?.reviewDue;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const left = this.lastProgressTouch(a.id) || '';
|
||||
const right = this.lastProgressTouch(b.id) || '';
|
||||
return right.localeCompare(left);
|
||||
})
|
||||
.slice(0, 4);
|
||||
},
|
||||
isLessonNumberValid() {
|
||||
return Number(this.newLesson.lessonNumber) > 0;
|
||||
@@ -269,29 +442,110 @@ export default {
|
||||
getLessonProgress(lessonId) {
|
||||
return this.progress.find(p => p.lessonId === lessonId);
|
||||
},
|
||||
lastProgressTouch(lessonId) {
|
||||
const progress = this.getLessonProgress(lessonId);
|
||||
return progress?.lastAccessedAt || progress?.completedAt || progress?.updatedAt || '';
|
||||
},
|
||||
daysSince(dateString) {
|
||||
if (!dateString) {
|
||||
return 0;
|
||||
}
|
||||
const timestamp = new Date(dateString).getTime();
|
||||
if (!Number.isFinite(timestamp)) {
|
||||
return 0;
|
||||
}
|
||||
const diff = Date.now() - timestamp;
|
||||
return Math.max(0, Math.floor(diff / (24 * 60 * 60 * 1000)));
|
||||
},
|
||||
formatDaysSince(dateString) {
|
||||
const days = this.daysSince(dateString);
|
||||
if (days <= 0) {
|
||||
return 'heute';
|
||||
}
|
||||
if (days === 1) {
|
||||
return 'seit 1 Tag';
|
||||
}
|
||||
return `seit ${days} Tagen`;
|
||||
},
|
||||
formatReviewDue(reviewNextDueAt) {
|
||||
if (!reviewNextDueAt) {
|
||||
return 'jetzt fällig';
|
||||
}
|
||||
const dueTimestamp = new Date(reviewNextDueAt).getTime();
|
||||
if (!Number.isFinite(dueTimestamp)) {
|
||||
return 'jetzt fällig';
|
||||
}
|
||||
const diffMs = dueTimestamp - Date.now();
|
||||
if (diffMs > 0) {
|
||||
const untilDays = Math.ceil(diffMs / (24 * 60 * 60 * 1000));
|
||||
if (untilDays <= 1) {
|
||||
return 'morgen fällig';
|
||||
}
|
||||
return `in ${untilDays} Tagen fällig`;
|
||||
}
|
||||
const diffDays = Math.floor((Date.now() - dueTimestamp) / (24 * 60 * 60 * 1000));
|
||||
if (diffDays <= 0) {
|
||||
return 'heute fällig';
|
||||
}
|
||||
if (diffDays === 1) {
|
||||
return 'seit 1 Tag fällig';
|
||||
}
|
||||
return `seit ${diffDays} Tagen fällig`;
|
||||
},
|
||||
getReviewStageLabel(progress) {
|
||||
const stage = Number(progress?.reviewStage || 0);
|
||||
if (stage === 0) return 'Tag 1';
|
||||
if (stage === 1) return 'Tag 3';
|
||||
if (stage === 2) return 'Tag 7';
|
||||
if (stage >= 3) return 'Review abgeschlossen';
|
||||
return '';
|
||||
},
|
||||
getReviewBadgeLabel(progress) {
|
||||
if (!progress?.completed) {
|
||||
return '';
|
||||
}
|
||||
const stageLabel = this.getReviewStageLabel(progress);
|
||||
if (!stageLabel) {
|
||||
return '';
|
||||
}
|
||||
if (progress.reviewCompleted) {
|
||||
return stageLabel;
|
||||
}
|
||||
const dueLabel = this.formatReviewDue(progress.reviewNextDueAt);
|
||||
return `${stageLabel} · ${dueLabel}`;
|
||||
},
|
||||
getReviewBadgeClass(progress) {
|
||||
if (!progress?.completed) {
|
||||
return '';
|
||||
}
|
||||
if (progress.reviewCompleted) {
|
||||
return 'review-badge--done';
|
||||
}
|
||||
if (progress.reviewDue) {
|
||||
return 'review-badge--due';
|
||||
}
|
||||
return 'review-badge--scheduled';
|
||||
},
|
||||
canStartLesson(lesson) {
|
||||
if (!this.course || !this.course.lessons) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Sortiere Lektionen nach lessonNumber
|
||||
const sortedLessons = [...this.course.lessons].sort((a, b) => a.lessonNumber - b.lessonNumber);
|
||||
|
||||
|
||||
// Finde den Index der aktuellen Lektion
|
||||
const currentIndex = sortedLessons.findIndex(l => l.id === lesson.id);
|
||||
|
||||
const currentIndex = this.sortedLessons.findIndex(l => l.id === lesson.id);
|
||||
|
||||
// Die erste Lektion kann immer gestartet werden
|
||||
if (currentIndex === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Wenn es nicht die erste Lektion ist, prüfe ob die vorherige abgeschlossen wurde
|
||||
if (currentIndex > 0) {
|
||||
const previousLesson = sortedLessons[currentIndex - 1];
|
||||
const previousLesson = this.sortedLessons[currentIndex - 1];
|
||||
const previousProgress = this.getLessonProgress(previousLesson.id);
|
||||
return previousProgress && previousProgress.completed;
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
},
|
||||
async addLesson() {
|
||||
@@ -406,6 +660,7 @@ export default {
|
||||
.course-hero,
|
||||
.course-info,
|
||||
.course-assistant,
|
||||
.course-flow,
|
||||
.lessons-list,
|
||||
.course-state {
|
||||
margin-bottom: 16px;
|
||||
@@ -475,6 +730,150 @@ export default {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.course-flow {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.course-flow__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.course-flow__eyebrow {
|
||||
display: inline-block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.course-flow__header h3,
|
||||
.course-flow__header p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.course-flow__header p {
|
||||
margin-top: 6px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.course-flow__stats {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.course-flow__stat {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(93, 64, 55, 0.08);
|
||||
color: #6d5446;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.course-flow__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.course-flow-card {
|
||||
padding: 16px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
|
||||
.course-flow-card__top {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.course-flow-card__top h4,
|
||||
.course-flow-card__top p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.course-flow-card__top p {
|
||||
margin-top: 4px;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.course-flow-card__badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 999px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.course-flow-card__badge--review {
|
||||
background: rgba(68, 138, 86, 0.16);
|
||||
color: #2f6b3d;
|
||||
}
|
||||
|
||||
.course-flow-card__badge--block {
|
||||
background: rgba(248, 162, 43, 0.18);
|
||||
color: #8a5411;
|
||||
}
|
||||
|
||||
.course-flow-card__badge--intensive {
|
||||
background: rgba(207, 78, 78, 0.16);
|
||||
color: #a13f3f;
|
||||
}
|
||||
|
||||
.course-flow-card__badge--practice {
|
||||
background: rgba(34, 96, 164, 0.14);
|
||||
color: #21598f;
|
||||
}
|
||||
|
||||
.course-flow-card__list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.course-flow-lesson {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: #fff;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.course-flow-lesson strong {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.course-flow-lesson span {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.84rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.course-flow-card__empty {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.share-code {
|
||||
font-family: monospace;
|
||||
}
|
||||
@@ -663,6 +1062,31 @@ export default {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.review-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 0.78em;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.review-badge--scheduled {
|
||||
background: rgba(34, 96, 164, 0.12);
|
||||
color: #21598f;
|
||||
}
|
||||
|
||||
.review-badge--due {
|
||||
background: rgba(185, 99, 24, 0.14);
|
||||
color: #8d5412;
|
||||
}
|
||||
|
||||
.review-badge--done {
|
||||
background: rgba(68, 138, 86, 0.14);
|
||||
color: #2f6b3d;
|
||||
}
|
||||
|
||||
.lesson-actions {
|
||||
display: block;
|
||||
}
|
||||
@@ -787,6 +1211,10 @@ export default {
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.course-flow__grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.course-assistant {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@@ -54,6 +54,17 @@
|
||||
<strong>{{ effectiveExercises?.length || 0 }} {{ $t('socialnetwork.vocab.courses.exercisesShort') }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="lessonReviewBadgeLabel"
|
||||
class="lesson-review-status"
|
||||
:class="lessonReviewStatusClass"
|
||||
>
|
||||
<div class="lesson-review-status__top">
|
||||
<span class="lesson-review-status__badge">{{ lessonReviewBadgeLabel }}</span>
|
||||
<strong>{{ lessonReviewHeadline }}</strong>
|
||||
</div>
|
||||
<p>{{ lessonReviewHint }}</p>
|
||||
</div>
|
||||
<details
|
||||
v-if="lessonPedagogy.phaseLabel || lessonPedagogy.newUnitTarget || lessonPedagogy.reviewWeight != null"
|
||||
class="lesson-overview-more"
|
||||
@@ -1212,6 +1223,57 @@ export default {
|
||||
isIntensiveReview: false
|
||||
};
|
||||
},
|
||||
lessonProgress() {
|
||||
return this.lesson?.progress || null;
|
||||
},
|
||||
lessonReviewBadgeLabel() {
|
||||
const progress = this.lessonProgress;
|
||||
if (!progress?.completed) {
|
||||
return '';
|
||||
}
|
||||
const stage = Number(progress.reviewStage || 0);
|
||||
if (stage === 0) return 'Tag 1';
|
||||
if (stage === 1) return 'Tag 3';
|
||||
if (stage === 2) return 'Tag 7';
|
||||
if (stage >= 3) return 'Review abgeschlossen';
|
||||
return '';
|
||||
},
|
||||
lessonReviewStatusClass() {
|
||||
const progress = this.lessonProgress;
|
||||
if (!progress?.completed) {
|
||||
return '';
|
||||
}
|
||||
if (progress.reviewCompleted) {
|
||||
return 'lesson-review-status--done';
|
||||
}
|
||||
if (progress.reviewDue) {
|
||||
return 'lesson-review-status--due';
|
||||
}
|
||||
return 'lesson-review-status--scheduled';
|
||||
},
|
||||
lessonReviewHeadline() {
|
||||
const progress = this.lessonProgress;
|
||||
if (!progress?.completed) {
|
||||
return '';
|
||||
}
|
||||
if (progress.reviewCompleted) {
|
||||
return 'Diese Lektion ist in der freien Vertiefung angekommen.';
|
||||
}
|
||||
if (progress.reviewDue) {
|
||||
return 'Diese Review-Welle ist jetzt fällig.';
|
||||
}
|
||||
return 'Diese Lektion ist für die nächste Review-Welle vorgemerkt.';
|
||||
},
|
||||
lessonReviewHint() {
|
||||
const progress = this.lessonProgress;
|
||||
if (!progress?.completed) {
|
||||
return '';
|
||||
}
|
||||
if (progress.reviewCompleted) {
|
||||
return 'Die 1/3/7-Tage-Wiederholung ist abgeschlossen. Du kannst die Lektion jetzt flexibel weitertrainieren.';
|
||||
}
|
||||
return `Nächste Fälligkeit: ${this.formatLessonReviewDue(progress.reviewNextDueAt)}.`;
|
||||
},
|
||||
assistantAvailable() {
|
||||
if (!this.assistantSettings) {
|
||||
return false;
|
||||
@@ -1993,6 +2055,31 @@ export default {
|
||||
}
|
||||
return this.$t('socialnetwork.vocab.courses.durationMinutes', { minutes });
|
||||
},
|
||||
formatLessonReviewDue(reviewNextDueAt) {
|
||||
if (!reviewNextDueAt) {
|
||||
return 'jetzt';
|
||||
}
|
||||
const dueTimestamp = new Date(reviewNextDueAt).getTime();
|
||||
if (!Number.isFinite(dueTimestamp)) {
|
||||
return 'jetzt';
|
||||
}
|
||||
const diffMs = dueTimestamp - Date.now();
|
||||
if (diffMs > 0) {
|
||||
const untilDays = Math.ceil(diffMs / (24 * 60 * 60 * 1000));
|
||||
if (untilDays <= 1) {
|
||||
return 'morgen';
|
||||
}
|
||||
return `in ${untilDays} Tagen`;
|
||||
}
|
||||
const overdueDays = Math.floor((Date.now() - dueTimestamp) / (24 * 60 * 60 * 1000));
|
||||
if (overdueDays <= 0) {
|
||||
return 'heute';
|
||||
}
|
||||
if (overdueDays === 1) {
|
||||
return 'seit 1 Tag';
|
||||
}
|
||||
return `seit ${overdueDays} Tagen`;
|
||||
},
|
||||
getQuestionData(exercise) {
|
||||
if (!exercise.questionData) return null;
|
||||
return typeof exercise.questionData === 'string'
|
||||
@@ -3040,6 +3127,61 @@ export default {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.lesson-review-status {
|
||||
padding: 14px 16px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(34, 96, 164, 0.18);
|
||||
background: rgba(235, 244, 255, 0.86);
|
||||
color: #21598f;
|
||||
}
|
||||
|
||||
.lesson-review-status__top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.lesson-review-status__badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(34, 96, 164, 0.14);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.lesson-review-status strong,
|
||||
.lesson-review-status p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.lesson-review-status p {
|
||||
margin-top: 8px;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.lesson-review-status--due {
|
||||
border-color: rgba(185, 99, 24, 0.22);
|
||||
background: rgba(255, 246, 226, 0.92);
|
||||
color: #8d5412;
|
||||
}
|
||||
|
||||
.lesson-review-status--due .lesson-review-status__badge {
|
||||
background: rgba(185, 99, 24, 0.16);
|
||||
}
|
||||
|
||||
.lesson-review-status--done {
|
||||
border-color: rgba(68, 138, 86, 0.22);
|
||||
background: rgba(239, 250, 242, 0.92);
|
||||
color: #2f6b3d;
|
||||
}
|
||||
|
||||
.lesson-review-status--done .lesson-review-status__badge {
|
||||
background: rgba(68, 138, 86, 0.14);
|
||||
}
|
||||
|
||||
.lesson-meta-label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
|
||||
Reference in New Issue
Block a user