From ac5d436a3674b205f3e5ba780c9634f4d519f485 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Wed, 1 Apr 2026 16:03:42 +0200 Subject: [PATCH] feat(vocab): implement scheduled review management in VocabService and UI updates - 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. --- backend/services/vocabService.js | 132 ++++- frontend/src/views/social/VocabCourseView.vue | 464 +++++++++++++++++- frontend/src/views/social/VocabLessonView.vue | 142 ++++++ 3 files changed, 716 insertions(+), 22 deletions(-) diff --git a/backend/services/vocabService.js b/backend/services/vocabService.js index 7b11b30..9d07935 100644 --- a/backend/services/vocabService.js +++ b/backend/services/vocabService.js @@ -15,6 +15,17 @@ import { Op } from 'sequelize'; import { BISAYA_PHASE1_DIDACTICS } from '../scripts/bisaya-course-phase1.js'; export default class VocabService { + _normalizeIsoDate(value) { + if (!value) { + return ''; + } + const date = value instanceof Date ? value : new Date(value); + if (Number.isNaN(date.getTime())) { + return ''; + } + return date.toISOString(); + } + _clampInteger(value, { min = 0, max = 100000, fallback = 0 } = {}) { const numeric = Number(value); if (!Number.isFinite(numeric)) { @@ -155,7 +166,10 @@ export default class VocabService { 'exerciseAnswers', 'exerciseResults', 'exerciseRetryPending', - 'exerciseRetryPendingSinceAttempts' + 'exerciseRetryPendingSinceAttempts', + 'reviewStage', + 'reviewNextDueAt', + 'reviewLastReviewedAt' ]; const hasKnownState = knownKeys.some((key) => Object.prototype.hasOwnProperty.call(value, key)); if (!hasKnownState) { @@ -185,10 +199,83 @@ export default class VocabService { exerciseAnswers: this._sanitizeExerciseAnswers(value.exerciseAnswers), exerciseResults: this._sanitizeExerciseResults(value.exerciseResults), exerciseRetryPending: Boolean(value.exerciseRetryPending), - exerciseRetryPendingSinceAttempts: this._clampInteger(value.exerciseRetryPendingSinceAttempts, { max: 10000 }) + exerciseRetryPendingSinceAttempts: this._clampInteger(value.exerciseRetryPendingSinceAttempts, { max: 10000 }), + reviewStage: this._clampInteger(value.reviewStage, { min: 0, max: 3 }), + reviewNextDueAt: this._normalizeIsoDate(value.reviewNextDueAt), + reviewLastReviewedAt: this._normalizeIsoDate(value.reviewLastReviewedAt) }; } + _supportsScheduledReview(lessonData = null) { + const lessonType = String(lessonData?.lessonType || '').toLowerCase(); + const didacticMode = String(lessonData?.didacticMode || '').toLowerCase(); + if (lessonType === 'culture' || lessonType === 'review' || lessonType === 'vocab_review') { + return false; + } + if (didacticMode === 'intensive_review' || didacticMode === 'checkpoint') { + return false; + } + return true; + } + + _removeManagedReviewState(lessonState = {}) { + const nextState = { ...(lessonState || {}) }; + delete nextState.reviewStage; + delete nextState.reviewNextDueAt; + delete nextState.reviewLastReviewedAt; + return nextState; + } + + _applyScheduledReviewState(lessonState = {}, { + previousCompleted = false, + nextCompleted = false, + shouldAdvanceReview = false, + lessonData = null, + now = new Date() + } = {}) { + const baseState = this._sanitizeLessonState(lessonState); + if (!this._supportsScheduledReview(lessonData) || !nextCompleted) { + return this._removeManagedReviewState(baseState); + } + + const reviewIntervalsDays = [1, 3, 7]; + const currentStage = this._clampInteger(baseState.reviewStage, { min: 0, max: reviewIntervalsDays.length }); + const dueAtIso = this._normalizeIsoDate(baseState.reviewNextDueAt); + const dueAt = dueAtIso ? new Date(dueAtIso) : null; + const reviewLastReviewedAt = this._normalizeIsoDate(baseState.reviewLastReviewedAt); + const nowIso = this._normalizeIsoDate(now); + + const nextState = { + ...baseState, + reviewStage: currentStage, + reviewNextDueAt: dueAtIso, + reviewLastReviewedAt + }; + + if (!previousCompleted && shouldAdvanceReview) { + nextState.reviewStage = 0; + nextState.reviewLastReviewedAt = nowIso; + nextState.reviewNextDueAt = this._normalizeIsoDate(new Date(now.getTime() + reviewIntervalsDays[0] * 24 * 60 * 60 * 1000)); + return nextState; + } + + if (!dueAtIso && currentStage < reviewIntervalsDays.length) { + nextState.reviewNextDueAt = this._normalizeIsoDate(new Date(now.getTime() + reviewIntervalsDays[currentStage] * 24 * 60 * 60 * 1000)); + } + + if (!shouldAdvanceReview || !dueAt || Number.isNaN(dueAt.getTime()) || now.getTime() < dueAt.getTime()) { + return nextState; + } + + const nextStage = Math.min(reviewIntervalsDays.length, currentStage + 1); + nextState.reviewStage = nextStage; + nextState.reviewLastReviewedAt = nowIso; + nextState.reviewNextDueAt = nextStage >= reviewIntervalsDays.length + ? '' + : this._normalizeIsoDate(new Date(now.getTime() + reviewIntervalsDays[nextStage] * 24 * 60 * 60 * 1000)); + return nextState; + } + _serializeLessonProgress(progress, lessonData = null) { if (!progress) { return null; @@ -197,13 +284,21 @@ export default class VocabService { const plainProgress = progress.get ? progress.get({ plain: true }) : { ...progress }; const targetScore = lessonData?.targetScorePercent || plainProgress.lesson?.targetScorePercent || 80; const hasReachedTarget = (plainProgress.score || 0) >= targetScore; + const lessonState = this._sanitizeLessonState(plainProgress.lessonState); + const reviewStage = this._clampInteger(lessonState.reviewStage, { min: 0, max: 3 }); + const reviewNextDueAt = this._normalizeIsoDate(lessonState.reviewNextDueAt); + const reviewDue = Boolean(reviewNextDueAt && reviewStage < 3 && new Date(reviewNextDueAt).getTime() <= Date.now()); return { ...plainProgress, - lessonState: this._sanitizeLessonState(plainProgress.lessonState), + lessonState, targetScore, hasReachedTarget, - needsReview: Boolean((lessonData?.requiresReview ?? plainProgress.lesson?.requiresReview) && !hasReachedTarget) + needsReview: Boolean((lessonData?.requiresReview ?? plainProgress.lesson?.requiresReview) && !hasReachedTarget), + reviewStage, + reviewNextDueAt, + reviewDue, + reviewCompleted: reviewStage >= 3 }; } @@ -2313,6 +2408,7 @@ export default class VocabService { const actualScore = Number(score) || 0; const hasReachedTarget = actualScore >= targetScore; const sanitizedLessonState = lessonState === undefined ? undefined : this._sanitizeLessonState(lessonState); + const didSubmitResult = completed !== undefined || score !== undefined; // Prüfe, ob Lektion als abgeschlossen gilt (nur wenn Ziel erreicht oder explizit completed=true) const isCompleted = Boolean(completed) || (hasReachedTarget && lessonData.requiresReview === false); @@ -2351,10 +2447,38 @@ export default class VocabService { if (sanitizedLessonState !== undefined) { updates.lessonState = sanitizedLessonState; } + const nextCompleted = updates.completed !== undefined ? Boolean(updates.completed) : Boolean(progress.completed); + const mergedLessonState = { + ...this._sanitizeLessonState(progress.lessonState), + ...(updates.lessonState || {}) + }; + updates.lessonState = this._applyScheduledReviewState(mergedLessonState, { + previousCompleted: Boolean(progress.completed), + nextCompleted, + shouldAdvanceReview: didSubmitResult && nextCompleted, + lessonData, + now: updates.completedAt || updates.lastAccessedAt || new Date() + }); await progress.update(updates); } else if (isCompleted) { progress.completed = true; progress.completedAt = new Date(); + progress.lessonState = this._applyScheduledReviewState(sanitizedLessonState || {}, { + previousCompleted: false, + nextCompleted: true, + shouldAdvanceReview: didSubmitResult, + lessonData, + now: progress.completedAt + }); + await progress.save(); + } else if (sanitizedLessonState !== undefined) { + progress.lessonState = this._applyScheduledReviewState(sanitizedLessonState, { + previousCompleted: false, + nextCompleted: false, + shouldAdvanceReview: false, + lessonData, + now: new Date() + }); await progress.save(); } diff --git a/frontend/src/views/social/VocabCourseView.vue b/frontend/src/views/social/VocabCourseView.vue index 5c9e8ec..5c24bbf 100644 --- a/frontend/src/views/social/VocabCourseView.vue +++ b/frontend/src/views/social/VocabCourseView.vue @@ -38,6 +38,112 @@ +
+
+
+ Tagesfluss +

Heute sinnvoll weitermachen

+

Die Reihenfolge folgt dem Konzept: fällige Wiederholung zuerst, dann aktueller Block, danach Intensivphase und freie Vertiefung.

+
+
+ Fällige Wiederholung: {{ dueReviewLessons.length }} + Aktiver Block: {{ currentBlockNumber || '—' }} +
+
+ +
+
+
+ 1 +
+

Fällige Wiederholung

+

Bereits abgeschlossene Lektionen, die heute wieder drankommen sollten.

+
+
+
+ +
+

Heute ist keine ältere Lektion als fällige Wiederholung markiert.

+
+ +
+
+ 2 +
+

Aktueller Block

+

Hier liegt der nächste reguläre Fortschritt im Kurs.

+
+
+
+ +
+

Der aktuelle Block ist bereits abgeschlossen oder es gibt gerade keine offene Blocklektion.

+
+ +
+
+ 3 +
+

Fällige Intensivphase

+

Verdichtete Wiederholung, sobald der Block davor weitgehend sitzt.

+
+
+
+ +
+

Aktuell ist keine neue Intensivphase freigeschaltet.

+
+ +
+
+ 4 +
+

Freie Vertiefung

+

Abgeschlossene Lektionen für lockeres Nachtrainieren außerhalb des Pflichtpfads.

+
+
+
+ +
+

Sobald du erste Lektionen abgeschlossen hast, erscheinen sie hier für freies Nachtrainieren.

+
+
+
+
@@ -54,7 +160,7 @@ {{ course.lessons.length }} Lektionen
-
+
#{{ lesson.lessonNumber }}
@@ -67,6 +173,13 @@ {{ $t('socialnetwork.vocab.courses.notStarted') }} + + {{ getReviewBadgeLabel(getLessonProgress(lesson.id)) }} +
@@ -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; } diff --git a/frontend/src/views/social/VocabLessonView.vue b/frontend/src/views/social/VocabLessonView.vue index afa87a4..0a59d40 100644 --- a/frontend/src/views/social/VocabLessonView.vue +++ b/frontend/src/views/social/VocabLessonView.vue @@ -54,6 +54,17 @@ {{ effectiveExercises?.length || 0 }} {{ $t('socialnetwork.vocab.courses.exercisesShort') }}
+
+
+ {{ lessonReviewBadgeLabel }} + {{ lessonReviewHeadline }} +
+

{{ lessonReviewHint }}

+
= 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;