- {#if !isQuizMode}
+ {#if showSummary || (completedStatus && completedStatus !== 'not_started' && !isQuizMode)}
+
Pertanyaan
@@ -134,55 +217,42 @@
onclick={() => handleOptionSelect(i)}
disabled={isAnswered}
>
-
- {String.fromCharCode(65 + i)}
-
-
- {@html option.text}
-
+
{String.fromCharCode(65 + i)}
+
{@html option.text}
{/each}
{#if isAnswered}
-
+
- {#if randomizedOptions[selectedOption!].is_correct}
- 🎉 Benar! Jawabanmu tepat.
+ {#if isCorrectArray[currentIndex]}
+ 🎉 Benar!
{:else}
- ❌ Kurang tepat. Coba pelajari lagi materinya nanti ya.
+ ❌ Kurang tepat.
{/if}
{#if currentCard.explanation}
-
- {@html currentCard.explanation}
-
+
{@html currentCard.explanation}
{/if}
{/if}
{:else}
-
Pertanyaan
-
- {@html currentCard.front}
-
+
{@html currentCard.front}
Klik untuk melihat jawaban
Jawaban
-
- {@html currentCard.back}
-
+
{@html currentCard.back}
{#if currentCard.explanation}
-
- {@html currentCard.explanation}
-
+
{@html currentCard.explanation}
{/if}
Klik untuk kembali ke pertanyaan
@@ -191,32 +261,21 @@
{/if}
-
-
+
{#if currentIndex < quizData.length - 1}
-
+
{:else}
-
+
{/if}
-
- {#if !isAllAnswered}
- 🔒 Selesaikan semua pertanyaan untuk dapat keluar
- {/if}
+
+ 🔒 Selesaikan kuis untuk melihat materi kembali
{:else}
-
-
Tidak ada data kuis untuk pelajaran ini.
-
+
{/if}
@@ -230,307 +289,90 @@
overflow-y: auto;
}
- .quiz-active-view {
- flex: 1;
- display: flex;
- flex-direction: column;
- }
-
- .quiz-start-view {
+ .summary-view {
text-align: center;
- max-width: 400px;
+ padding: 2rem 1rem;
+ max-width: 500px;
margin: auto;
- display: flex;
- flex-direction: column;
- gap: 1rem;
}
+ .summary-icon { font-size: 4rem; margin-bottom: 1rem; }
+ .score-display { font-size: 1.5rem; margin-bottom: 1.5rem; }
+ .score-display strong { color: var(--color-primary); font-size: 2.5rem; }
- .quiz-icon {
- font-size: 4rem;
- margin-bottom: 0.5rem;
- }
-
- .alert {
- padding: 1rem;
- border-radius: 8px;
- font-size: 0.9rem;
- line-height: 1.4;
+ .wrong-topics {
text-align: left;
+ margin-top: 2rem;
+ padding-top: 2rem;
+ border-top: 1px solid var(--color-border);
}
+ .wrong-topics h3 { font-size: 1rem; margin-bottom: 1rem; color: var(--color-danger); }
+ .wrong-topics ul { padding-left: 1.5rem; }
+ .wrong-topics li { margin-bottom: 0.75rem; font-size: 0.9rem; }
- .alert-warning {
- background: #fff9db;
- border: 1px solid #fab005;
- color: #862e00;
- }
+ .quiz-active-view { flex: 1; display: flex; flex-direction: column; }
+ .quiz-start-view { text-align: center; max-width: 400px; margin: auto; display: flex; flex-direction: column; gap: 1rem; }
+ .quiz-icon { font-size: 4rem; margin-bottom: 0.5rem; }
- .quiz-progress {
- font-size: 0.85rem;
- color: var(--color-text-muted);
- margin-bottom: 1.5rem;
- }
+ .alert { padding: 1rem; border-radius: 8px; font-size: 0.9rem; line-height: 1.4; text-align: left; }
+ .alert-warning { background: var(--color-bg-secondary); border: 1px solid var(--color-warning); color: var(--color-text); }
+ .alert-success { background: rgba(25, 135, 84, 0.1); border: 1px solid var(--color-success); color: var(--color-success); }
+ .alert-info { background: rgba(13, 110, 253, 0.1); border: 1px solid var(--color-primary); color: var(--color-primary); }
- .progress-bar-bg {
- height: 6px;
- background: var(--color-border);
- border-radius: 3px;
- margin-top: 0.5rem;
- overflow: hidden;
- }
+ .quiz-progress { font-size: 0.85rem; color: var(--color-text-muted); margin-bottom: 1.5rem; }
+ .progress-bar-bg { height: 6px; background: var(--color-border); border-radius: 3px; margin-top: 0.5rem; overflow: hidden; }
+ .progress-bar-fill { height: 100%; background: var(--color-primary, #339af0); transition: width 0.3s ease; }
- .progress-bar-fill {
- height: 100%;
- background: var(--color-primary, #339af0);
- transition: width 0.3s ease;
- }
-
- /* MCQ Styling */
- .mcq-view {
- flex: 1;
- display: flex;
- flex-direction: column;
- gap: 1.5rem;
- margin-bottom: 2rem;
- }
-
- .question-box {
- background: white;
- border: 2px solid var(--color-border);
- border-radius: 12px;
- padding: 1.5rem;
- }
-
- .options-grid {
- display: flex;
- flex-direction: column;
- gap: 0.75rem;
- }
-
- .option-btn {
- display: flex;
- align-items: center;
- gap: 1rem;
- padding: 1rem;
- background: white;
- border: 2px solid var(--color-border);
- border-radius: 12px;
- cursor: pointer;
- text-align: left;
- transition: all 0.2s;
- font-size: 1rem;
- color: var(--color-text);
- }
-
- .option-btn:hover:not(:disabled) {
- border-color: var(--color-primary);
- background: #f1f9ff;
- }
-
- .option-btn.selected {
- border-color: var(--color-primary);
- background: #e7f5ff;
- }
-
- .option-btn.correct {
- border-color: #40c057;
- background: #ebfbee;
- }
-
- .option-btn.wrong {
- border-color: #fa5252;
- background: #fff5f5;
- }
-
- .option-marker {
- width: 32px;
- height: 32px;
- display: flex;
- align-items: center;
- justify-content: center;
- background: #f1f3f5;
- border-radius: 8px;
- font-weight: 700;
- font-size: 0.9rem;
- flex-shrink: 0;
- }
-
- .option-btn.correct .option-marker { background: #40c057; color: white; }
- .option-btn.wrong .option-marker { background: #fa5252; color: white; }
-
- .option-text {
- flex: 1;
- }
-
- .feedback-area {
- padding: 1.25rem;
- border-radius: 12px;
- display: flex;
- flex-direction: column;
- gap: 0.75rem;
- }
-
- .feedback-area.correct { color: #2b8a3e; background: #ebfbee; }
- .feedback-area:not(.correct) { color: #c92a2a; background: #fff5f5; }
-
- .explanation-box {
- font-size: 0.85rem;
- line-height: 1.5;
- padding: 0.75rem;
- background: rgba(255, 255, 255, 0.5);
- border-radius: 8px;
- text-align: left;
- }
-
- .explanation-box.small {
- margin-top: 1rem;
- max-width: 100%;
- font-size: 0.75rem;
- }
+ .mcq-view { flex: 1; display: flex; flex-direction: column; gap: 1.5rem; margin-bottom: 2rem; }
+ .question-box { background: var(--color-bg-secondary); border: 2px solid var(--color-border); border-radius: 12px; padding: 1.5rem; }
+ .options-grid { display: flex; flex-direction: column; gap: 0.75rem; }
+ .option-btn { display: flex; align-items: center; gap: 1rem; padding: 1rem; background: var(--color-bg); border: 2px solid var(--color-border); border-radius: 12px; cursor: pointer; text-align: left; transition: all 0.2s; font-size: 1rem; color: var(--color-text); }
+ .option-btn:hover:not(:disabled) { border-color: var(--color-primary); background: var(--color-bg-secondary); }
+ .option-btn.selected { border-color: var(--color-primary); background: var(--color-bg-secondary); }
+ .option-btn.correct { border-color: var(--color-success); background: rgba(25, 135, 84, 0.15); }
+ .option-btn.wrong { border-color: var(--color-danger); background: rgba(220, 53, 69, 0.15); }
+ .option-marker { width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; background: var(--color-bg-secondary); border: 1px solid var(--color-border); border-radius: 8px; font-weight: 700; font-size: 0.9rem; flex-shrink: 0; color: var(--color-text); }
+ .option-btn.correct .option-marker { background: var(--color-success); border-color: var(--color-success); color: white; }
+ .option-btn.wrong .option-marker { background: var(--color-danger); border-color: var(--color-danger); color: white; }
+ .option-text { flex: 1; }
+ .feedback-area { padding: 1.25rem; border-radius: 12px; display: flex; flex-direction: column; gap: 0.75rem; }
+ .feedback-area.correct { color: var(--color-success); background: rgba(25, 135, 84, 0.1); border: 1px solid rgba(25, 135, 84, 0.2); }
+ .feedback-area:not(.correct) { color: var(--color-danger); background: rgba(220, 53, 69, 0.1); border: 1px solid rgba(220, 53, 69, 0.2); }
+ .explanation-box { font-size: 0.85rem; line-height: 1.5; padding: 1rem; background: var(--color-bg-secondary); border: 1px solid var(--color-border); border-radius: 8px; text-align: left; }
+ .explanation-box.small { margin-top: 1rem; max-width: 100%; font-size: 0.75rem; }
.explanation-box :global(p) { margin: 0; }
- /* Flashcard Styling */
- .flashcard-wrapper {
- flex: 1;
- perspective: 1000px;
- min-height: 250px;
- cursor: pointer;
- margin-bottom: 2rem;
- }
+ .flashcard-wrapper { flex: 1; perspective: 1000px; min-height: 250px; cursor: pointer; margin-bottom: 2rem; }
+ .flashcard { position: relative; width: 100%; height: 100%; transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1); transform-style: preserve-3d; }
+ .flashcard.flipped { transform: rotateY(180deg); }
+ .flashcard-front, .flashcard-back { position: absolute; width: 100%; height: 100%; backface-visibility: hidden; display: flex; flex-direction: column; padding: 2rem; background: var(--color-bg-secondary); border: 2px solid var(--color-border); border-radius: 16px; box-shadow: var(--shadow); }
+ .flashcard-back { transform: rotateY(180deg); background: var(--color-bg); }
+ .side-label { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--color-text-muted); margin-bottom: 1rem; font-weight: 700; border-bottom: 1px solid var(--color-border); padding-bottom: 0.5rem; }
+ .card-content { flex: 1; display: flex; flex-direction: column; justify-content: center; align-items: center; text-align: center; font-size: 1.25rem; color: var(--color-text); line-height: 1.5; }
+ :global(.card-content code) { background: var(--color-bg); border: 1px solid var(--color-border); padding: 0.2rem 0.4rem; border-radius: 4px; font-family: var(--font-mono); }
+ :global(.card-content p) { margin: 0; }
+ .flip-hint { font-size: 0.75rem; color: var(--color-text-muted); margin-top: 1rem; font-style: italic; }
- .flashcard {
+ .quiz-controls {
+ display: flex;
+ justify-content: space-between;
+ gap: 1rem;
+ margin-top: auto;
position: relative;
- width: 100%;
- height: 100%;
- transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
- transform-style: preserve-3d;
+ z-index: 10; /* Ensure buttons are clickable */
+ background: var(--color-bg); /* Prevents card content from showing through during scroll */
+ padding-top: 1rem;
}
-
- .flashcard.flipped {
- transform: rotateY(180deg);
- }
-
- .flashcard-front, .flashcard-back {
- position: absolute;
- width: 100%;
- height: 100%;
- backface-visibility: hidden;
- display: flex;
- flex-direction: column;
- padding: 2rem;
- background: white;
- border: 2px solid var(--color-border);
- border-radius: 16px;
- box-shadow: 0 4px 12px rgba(0,0,0,0.05);
- }
-
- .flashcard-back {
- transform: rotateY(180deg);
- background: #f8f9fa;
- }
-
- .side-label {
- font-size: 0.7rem;
- text-transform: uppercase;
- letter-spacing: 0.05em;
- color: var(--color-text-muted);
- margin-bottom: 1rem;
- font-weight: 700;
- }
-
- .card-content {
- flex: 1;
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- text-align: center;
- font-size: 1.25rem;
- color: var(--color-text);
- line-height: 1.5;
- }
-
- :global(.card-content code) {
- background: #eee;
- padding: 0.2rem 0.4rem;
- border-radius: 4px;
- font-family: monospace;
- }
-
- :global(.card-content p) {
- margin: 0;
- }
-
- .flip-hint {
- font-size: 0.75rem;
- color: var(--color-text-muted);
- margin-top: 1rem;
- font-style: italic;
- }
-
- .quiz-controls {
- display: flex;
- justify-content: space-between;
- gap: 1rem;
- margin-top: auto;
- }
-
- .btn {
- padding: 0.6rem 1.2rem;
- border-radius: 8px;
- font-weight: 600;
- cursor: pointer;
- border: none;
- transition: opacity 0.2s;
- }
-
- .btn:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- }
-
+ .btn { padding: 0.6rem 1.2rem; border-radius: 8px; font-weight: 600; cursor: pointer; border: none; transition: opacity 0.2s; }
+ .btn:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-primary { background: #339af0; color: white; }
.btn-success { background: #40c057; color: white; }
.btn-outline { background: white; border: 1px solid var(--color-border); color: var(--color-text); }
.btn-lg { padding: 1rem 2rem; font-size: 1.1rem; }
- .btn-exit-quiz {
- background: none;
- border: none;
- color: var(--color-text-muted);
- text-decoration: underline;
- font-size: 0.85rem;
- cursor: pointer;
- }
-
- .btn-exit-quiz:disabled {
- color: #adb5bd;
- text-decoration: none;
- cursor: not-allowed;
- }
-
- .cancel-container {
- display: flex;
- flex-direction: column;
- align-items: center;
- margin-top: 1rem;
- gap: 0.5rem;
- }
-
- .cancel-lock-hint {
- font-size: 0.75rem;
- color: #fa5252;
- font-weight: 500;
- }
-
- .quiz-empty {
- text-align: center;
- margin: auto;
- color: var(--color-text-muted);
- }
-
- @media (max-width: 600px) {
- .card-content { font-size: 1.1rem; }
- .option-btn { padding: 0.75rem; font-size: 0.95rem; }
- }
+ .btn-exit-quiz { background: none; border: none; color: var(--color-text-muted); text-decoration: underline; font-size: 0.85rem; cursor: pointer; }
+ .cancel-container { display: flex; flex-direction: column; align-items: center; margin-top: 1rem; gap: 0.5rem; }
+ .cancel-lock-hint { font-size: 0.75rem; color: #fa5252; font-weight: 500; }
+ .quiz-empty { text-align: center; margin: auto; color: var(--color-text-muted); }
+ @media (max-width: 600px) { .card-content { font-size: 1.1rem; } .option-btn { padding: 0.75rem; font-size: 0.95rem; } }
diff --git a/frontend/src/routes/lesson/[slug]/lesson.svelte.ts b/frontend/src/routes/lesson/[slug]/lesson.svelte.ts
index 11ad050..c6f521d 100644
--- a/frontend/src/routes/lesson/[slug]/lesson.svelte.ts
+++ b/frontend/src/routes/lesson/[slug]/lesson.svelte.ts
@@ -199,12 +199,13 @@ export class LessonManager {
});
}
- async completeLesson() {
- if (this.lessonCompleted) return;
+ async completeLesson(status = 'completed') {
+ if (this.lessonCompleted && status === 'completed') return;
this.showCelebration = true;
+
if (get(authLoggedIn)) {
const lessonName = this.slug.replace('.md', '');
- await trackProgress(get(auth).token, lessonName);
+ await trackProgress(auth.token, lessonName, status);
this.lessonCompleted = true;
lessonContext.update(ctx => ctx ? { ...ctx, completed: true } : ctx);
}
@@ -298,7 +299,7 @@ export class LessonManager {
this.activeTab = 'output';
try {
const code = (this.currentLanguage === lang) ? (this.editor?.getCode() ?? this.currentCode) : (lang === 'c' ? this.cCode : this.pythonCode);
- const res = await compileCode({ code, language: lang, token: get(auth).token });
+ const res = await compileCode({ code, language: lang, token: auth.token });
if (!res.success) {
Object.assign(out, { error: res.error || 'Compilation failed', success: false });
return;
diff --git a/frontend/src/routes/progress/+page.svelte b/frontend/src/routes/progress/+page.svelte
index 10214cb..3f5eeef 100644
--- a/frontend/src/routes/progress/+page.svelte
+++ b/frontend/src/routes/progress/+page.svelte
@@ -18,6 +18,10 @@
let loading = $state(true);
onMount(async () => {
+ await loadData();
+ });
+
+ async function loadData() {
if (!$authIsTeacher) {
loading = false;
return;
@@ -33,7 +37,33 @@
} finally {
loading = false;
}
- });
+ }
+
+ async function handleReset(studentToken: string, lessonName: string, studentName: string) {
+ if (!window.confirm(`Apakah Anda yakin ingin me-reset progres kuis "${lessonName}" untuk siswa "${studentName}"?`)) {
+ return;
+ }
+
+ try {
+ const res = await fetch('/api/reset-progress', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ teacher_token: auth.token,
+ student_token: studentToken,
+ lesson_name: lessonName
+ })
+ });
+ const data = await res.json();
+ if (data.success) {
+ await loadData();
+ } else {
+ alert('Gagal me-reset: ' + data.message);
+ }
+ } catch (err) {
+ alert('Terjadi kesalahan saat menghubungi server.');
+ }
+ }
const totalLessons = $derived(lessons.length);
@@ -78,11 +108,25 @@
{@const key = lesson.filename.replace('.md', '')}
{@const status = student[key]}
- {#if status === 'completed'}
- ✓
- {:else}
- —
- {/if}
+
+ {#if status === 'completed'}
+ ✓
+ {:else if status && status !== 'not_started'}
+ {status}
+ {:else}
+ —
+ {/if}
+
+ {#if status && status !== 'not_started'}
+
+ {/if}
+
|
{/each}
@@ -172,9 +216,36 @@
color: var(--color-success);
font-weight: bold;
}
+ .badge.score {
+ color: var(--color-primary);
+ font-weight: 600;
+ font-size: 0.75rem;
+ }
.badge.empty {
color: var(--color-text-muted);
}
+ .cell-content {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.4rem;
+ }
+ .btn-reset-mini {
+ background: none;
+ border: 1px solid var(--color-border);
+ border-radius: 4px;
+ color: var(--color-text-muted);
+ cursor: pointer;
+ font-size: 0.7rem;
+ padding: 0 0.15rem;
+ line-height: 1.1;
+ transition: all 0.2s;
+ }
+ .btn-reset-mini:hover {
+ border-color: #fa5252;
+ color: #fa5252;
+ background: #fff5f5;
+ }
.completion-count {
font-weight: 600;
min-width: 60px;
diff --git a/routes/lessons.py b/routes/lessons.py
index d0f75b4..74145ea 100644
--- a/routes/lessons.py
+++ b/routes/lessons.py
@@ -108,10 +108,13 @@ def api_lesson(filename):
token = request.args.get('token', '') or request.cookies.get('student_token', '')
progress = None
lesson_completed = False
+ lesson_progress_status = ''
if token:
progress = get_student_progress(token)
- if progress and full_filename.replace('.md', '') in progress:
- lesson_completed = progress[full_filename.replace('.md', '')] == 'completed'
+ if progress:
+ status = progress.get(full_filename.replace('.md', ''), '')
+ lesson_progress_status = status
+ lesson_completed = status not in (None, '', 'not_started')
all_lessons = get_ordered_lessons_with_learning_objectives(progress)
@@ -194,6 +197,7 @@ def api_lesson(filename):
'key_text_circuit': key_text_circuit,
'active_tabs': active_tabs,
'quiz_data': quiz_data,
+ 'lesson_progress_status': lesson_progress_status,
'lesson_title': full_filename.replace('.md', '').replace('_', ' ').title(),
'lesson_completed': lesson_completed,
'locked': is_locked,
diff --git a/routes/progress.py b/routes/progress.py
index ffae05f..47a109f 100644
--- a/routes/progress.py
+++ b/routes/progress.py
@@ -49,6 +49,35 @@ def track_progress():
return jsonify({'success': False, 'message': f'Error tracking progress: {e}'})
+@progress_bp.route('/reset-progress', methods=['POST'])
+def reset_progress():
+ """Reset student progress for a lesson (Teacher only)."""
+ try:
+ data = request.get_json()
+ teacher_token = data.get('teacher_token', '').strip()
+ student_token = data.get('student_token', '').strip()
+ lesson_name = data.get('lesson_name', '').strip()
+
+ if not teacher_token or not student_token or not lesson_name:
+ return jsonify({'success': False, 'message': 'All fields are required'}), 400
+
+ # Validate teacher token
+ teacher_info = validate_token(teacher_token)
+ if not teacher_info or not teacher_info.get('is_teacher'):
+ return jsonify({'success': False, 'message': 'Unauthorized (Teacher only)'}), 401
+
+ # Perform reset (set to not_started)
+ updated = update_student_progress(student_token, lesson_name, 'not_started')
+ if updated:
+ return jsonify({'success': True, 'message': 'Progress reset successfully'})
+ else:
+ return jsonify({'success': False, 'message': 'Failed to reset progress'})
+
+ except Exception as e:
+ logging.error(f"Error in reset-progress: {e}")
+ return jsonify({'success': False, 'message': f'Error resetting progress: {e}'})
+
+
@progress_bp.route('/progress-report.json')
def api_progress_report():
"""Return progress report data as JSON."""
diff --git a/services/lesson_service.py b/services/lesson_service.py
index 4624901..191bbb5 100644
--- a/services/lesson_service.py
+++ b/services/lesson_service.py
@@ -191,7 +191,8 @@ def get_ordered_lessons_with_learning_objectives(progress=None):
def _add_completion_and_prereqs(lesson, progress):
slug = lesson['filename'].replace('.md', '')
if progress:
- lesson['completed'] = progress.get(slug) == 'completed'
+ status = progress.get(slug, '')
+ lesson['completed'] = status not in (None, '', 'not_started')
else:
lesson['completed'] = False
diff --git a/services/token_service.py b/services/token_service.py
index 4ac6c0b..ffcd247 100644
--- a/services/token_service.py
+++ b/services/token_service.py
@@ -172,7 +172,8 @@ def calculate_student_completion(student_data, all_lessons):
else:
lesson_key = lesson.replace('.md', '')
- if lesson_key in student_data and student_data[lesson_key] == 'completed':
+ status = student_data.get(lesson_key, '')
+ if status and status not in ('not_started', ''):
completed_count += 1
return completed_count
@@ -204,11 +205,7 @@ def get_all_students_progress(all_lessons_func):
})
for row in tokens.values():
- student_data = dict(row)
- # Don't delete 'token' from the original dict in cache!
- student_data_copy = student_data.copy()
- if 'token' in student_data_copy:
- del student_data_copy['token']
+ student_data_copy = dict(row)
student_data_copy['completed_count'] = calculate_student_completion(student_data_copy, ordered_lessons)
all_students_progress.append(student_data_copy)
|