Skip to content

Commit 5da6280

Browse files
committed
feat: enhance LessonEngine to manage user progress and code storage
1 parent 229fb19 commit 5da6280

File tree

2 files changed

+229
-160
lines changed

2 files changed

+229
-160
lines changed

src/app.js

Lines changed: 62 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,9 @@
11
import { LessonEngine } from "./impl/LessonEngine.js";
22
import { renderLesson, renderModuleList, renderLevelIndicator, showFeedback, updateActiveLessonInSidebar } from "./helpers/renderer.js";
3-
import { validateUserCode } from "./helpers/validator.js";
43
import { loadModules } from "./config/lessons.js";
54

6-
// Main Application state
5+
// Simplified state - LessonEngine now manages lesson state and progress
76
const state = {
8-
currentModule: null,
9-
currentLessonIndex: 0,
10-
modules: [],
11-
userCode: new Map(), // Store user code for each lesson
12-
userProgress: {}, // Format: { moduleId: { completed: [0, 2, 3], current: 4 } }
13-
userCodeBeforeValidation: "", // Track user code state before validation
147
userSettings: {
158
disableFeedbackErrors: false
169
}
@@ -44,7 +37,7 @@ const elements = {
4437
disableFeedbackToggle: document.getElementById("disable-feedback-toggle")
4538
};
4639

47-
// Initialize the lesson engine
40+
// Initialize the lesson engine - now the single source of truth
4841
const lessonEngine = new LessonEngine();
4942

5043
// Load user progress from localStorage
@@ -89,17 +82,20 @@ function initFeedbackToggle() {
8982
// Initialize the module list
9083
async function initializeModules() {
9184
try {
92-
state.modules = await loadModules();
85+
const modules = await loadModules();
86+
lessonEngine.setModules(modules);
9387

9488
// Use the new renderModuleList function with both callbacks
95-
renderModuleList(elements.moduleList, state.modules, selectModule, selectLesson);
89+
renderModuleList(elements.moduleList, modules, selectModule, selectLesson);
9690

97-
// Select the first module or the last one user was on
98-
const lastModuleId = localStorage.getItem("codeCrispies.lastModuleId");
99-
if (lastModuleId && state.modules.find((m) => m.id === lastModuleId)) {
91+
// Load saved progress and select appropriate module
92+
const progressData = lessonEngine.loadUserProgress();
93+
const lastModuleId = progressData?.lastModuleId;
94+
95+
if (lastModuleId && modules.find(m => m.id === lastModuleId)) {
10096
selectModule(lastModuleId);
101-
} else if (state.modules.length > 0) {
102-
selectModule(state.modules[0].id);
97+
} else if (modules.length > 0) {
98+
selectModule(modules[0].id);
10399
}
104100

105101
// Update progress indicator on module selector button
@@ -112,21 +108,7 @@ async function initializeModules() {
112108

113109
// Update progress indicator on module selector button
114110
function updateModuleSelectorButtonProgress() {
115-
if (!state.modules.length) return;
116-
117-
// Calculate overall progress across all modules
118-
let totalLessons = 0;
119-
let totalCompleted = 0;
120-
121-
state.modules.forEach((module) => {
122-
totalLessons += module.lessons.length;
123-
const progress = state.userProgress[module.id];
124-
if (progress && progress.completed) {
125-
totalCompleted += progress.completed.length;
126-
}
127-
});
128-
129-
const percentComplete = totalLessons > 0 ? Math.round((totalCompleted / totalLessons) * 100) : 0;
111+
const stats = lessonEngine.getProgressStats();
130112

131113
// Create progress indicator
132114
const progressBar = document.createElement("div");
@@ -136,13 +118,13 @@ function updateModuleSelectorButtonProgress() {
136118
bottom: 0;
137119
left: 0;
138120
height: 3px;
139-
width: ${percentComplete}%;
121+
width: ${stats.percentComplete}%;
140122
background-color: var(--primary-light);
141123
border-radius: 0 3px 3px 0;
142124
`;
143125

144126
// Add progress percentage text
145-
elements.moduleSelectorBtn.innerHTML = `Progress <span style="font-size: 0.8em; opacity: 0.8;">${percentComplete}%</span>`;
127+
elements.moduleSelectorBtn.innerHTML = `Progress <span style="font-size: 0.8em; opacity: 0.8;">${stats.percentComplete}%</span>`;
146128
elements.moduleSelectorBtn.style.position = "relative";
147129

148130
// Remove any existing progress bar before adding new one
@@ -154,12 +136,10 @@ function updateModuleSelectorButtonProgress() {
154136
elements.moduleSelectorBtn.appendChild(progressBar);
155137
}
156138

157-
// Select a module
139+
// Select a module - delegate to LessonEngine
158140
function selectModule(moduleId) {
159-
const selectedModule = state.modules.find((module) => module.id === moduleId);
160-
if (!selectedModule) return;
161-
162-
state.currentModule = selectedModule;
141+
const success = lessonEngine.setModuleById(moduleId);
142+
if (!success) return;
163143

164144
// Update module list UI to highlight the active module
165145
const moduleItems = elements.moduleList.querySelectorAll(".module-header");
@@ -170,35 +150,21 @@ function selectModule(moduleId) {
170150
}
171151
});
172152

173-
// Load user progress for this module
174-
if (!state.userProgress[moduleId]) {
175-
state.userProgress[moduleId] = { completed: [], current: 0 };
176-
}
177-
178-
state.currentLessonIndex = state.userProgress[moduleId].current || 0;
179153
loadCurrentLesson();
180154

181-
// Save the last selected module
182-
localStorage.setItem("codeCrispies.lastModuleId", moduleId);
183-
184155
// Reset any success indicators
185156
resetSuccessIndicators();
186157
}
187158

188159
function selectLesson(moduleId, lessonIndex) {
189160
// Select the module first if it's not already selected
190-
if (!state.currentModule || state.currentModule.id !== moduleId) {
191-
selectModule(moduleId);
161+
const currentState = lessonEngine.getCurrentState();
162+
if (!currentState.module || currentState.module.id !== moduleId) {
163+
lessonEngine.setModuleById(moduleId);
192164
}
193165

194-
// Update current lesson index
195-
state.currentLessonIndex = lessonIndex;
196-
197-
// Update user progress
198-
state.userProgress[moduleId].current = lessonIndex;
199-
saveUserProgress();
200-
201-
// Load the lesson
166+
// Set the lesson
167+
lessonEngine.setLessonByIndex(lessonIndex);
202168
loadCurrentLesson();
203169
}
204170

@@ -229,22 +195,16 @@ function resetEditorLayout(lesson) {
229195
elements.validationIndicators.innerHTML = "";
230196
}
231197

232-
// Load the current lesson
198+
// Load the current lesson - now delegates to LessonEngine
233199
function loadCurrentLesson() {
234-
if (!state.currentModule || !state.currentModule.lessons) {
235-
return;
236-
}
200+
const engineState = lessonEngine.getCurrentState();
237201

238-
// Make sure lesson index is in bounds
239-
if (state.currentLessonIndex >= state.currentModule.lessons.length) {
240-
state.currentLessonIndex = state.currentModule.lessons.length - 1;
241-
} else if (state.currentLessonIndex < 0) {
242-
state.currentLessonIndex = 0;
202+
if (!engineState.module || !engineState.lesson) {
203+
return;
243204
}
244205

245-
const lesson = state.currentModule.lessons[state.currentLessonIndex];
246-
const mode = lesson.mode || state.currentModule?.mode || "css";
247-
lessonEngine.setLesson(lesson);
206+
const lesson = engineState.lesson;
207+
const mode = lesson.mode || engineState.module?.mode || "css";
248208

249209
// Update UI based on mode
250210
updateEditorForMode(mode);
@@ -264,12 +224,14 @@ function loadCurrentLesson() {
264224
lesson
265225
);
266226

227+
// Set user code in input
228+
elements.codeInput.value = engineState.userCode;
229+
267230
// Configure editor layout based on lesson settings
268231
resetEditorLayout(lesson);
269232

270233
// Update Run button text based on completion status
271-
const moduleProgress = state.userProgress[state.currentModule.id];
272-
if (moduleProgress && moduleProgress.completed.includes(state.currentLessonIndex)) {
234+
if (engineState.isCompleted) {
273235
elements.runBtn.innerHTML = '<img src="./gear.svg" />Re-run';
274236

275237
// Add completion badge next to title if not already present
@@ -290,27 +252,20 @@ function loadCurrentLesson() {
290252
}
291253

292254
// Update level indicator
293-
renderLevelIndicator(elements.levelIndicator, state.currentLessonIndex + 1, state.currentModule.lessons.length);
255+
renderLevelIndicator(elements.levelIndicator, engineState.lessonIndex + 1, engineState.totalLessons);
294256

295257
// Update active lesson in sidebar
296-
updateActiveLessonInSidebar(state.currentModule.id, state.currentLessonIndex);
258+
updateActiveLessonInSidebar(engineState.module.id, engineState.lessonIndex);
297259

298260
// Update navigation buttons
299261
updateNavigationButtons();
300262

301-
// Save current progress
302-
state.userProgress[state.currentModule.id].current = state.currentLessonIndex;
303-
saveUserProgress();
304-
305263
// Update progress indicator on module selector button
306264
updateModuleSelectorButtonProgress();
307265

308266
// Focus on the code editor by default
309267
elements.codeInput.focus();
310268

311-
// Store current code
312-
state.userCodeBeforeValidation = elements.codeInput.value;
313-
314269
// Track live changes and update preview when the user pauses typing
315270
setupLivePreview();
316271
}
@@ -334,19 +289,16 @@ function handleUserInput() {
334289

335290
// Set a new timer for preview update after user stops typing
336291
previewTimer = setTimeout(() => {
337-
// Apply the code for preview without validation
338-
// lessonEngine.applyUserCode(elements.codeInput.value);
339292
runCode();
340-
}, 800); // Update preview 500ms after user stops typing
341-
342-
// Store current code state
343-
state.userCodeBeforeValidation = elements.codeInput.value;
293+
}, 800); // Update preview 800ms after user stops typing
344294
}
345295

346296
// Update navigation buttons state
347297
function updateNavigationButtons() {
348-
elements.prevBtn.disabled = state.currentLessonIndex === 0;
349-
elements.nextBtn.disabled = !state.currentModule || state.currentLessonIndex === state.currentModule.lessons.length - 1;
298+
const engineState = lessonEngine.getCurrentState();
299+
300+
elements.prevBtn.disabled = !engineState.canGoPrev;
301+
elements.nextBtn.disabled = !engineState.canGoNext;
350302

351303
// Style changes for disabled buttons
352304
if (elements.prevBtn.disabled) {
@@ -362,42 +314,36 @@ function updateNavigationButtons() {
362314
}
363315
}
364316

365-
// Go to the next lesson
317+
// Go to the next lesson - delegate to LessonEngine
366318
function nextLesson() {
367-
if (!state.currentModule) return;
368-
369-
if (state.currentLessonIndex < state.currentModule.lessons.length - 1) {
370-
state.currentLessonIndex++;
319+
const success = lessonEngine.nextLesson();
320+
if (success) {
371321
loadCurrentLesson();
372322
}
373323
}
374324

375-
// Go to the previous lesson
325+
// Go to the previous lesson - delegate to LessonEngine
376326
function prevLesson() {
377-
if (state.currentLessonIndex > 0) {
378-
state.currentLessonIndex--;
327+
const success = lessonEngine.previousLesson();
328+
if (success) {
379329
loadCurrentLesson();
380330
}
381331
}
382332

383-
// Run the user code
333+
// Run the user code - now uses LessonEngine validation
384334
function runCode() {
385335
const userCode = elements.codeInput.value;
386-
const lesson = state.currentModule.lessons[state.currentLessonIndex];
387336

388337
// Rotate the Run button icon
389338
const runButtonImg = document.querySelector("#run-btn img");
390339
const runButtonRotationDegree = Number(runButtonImg.style.transform.match(/\d+/)?.pop() ?? 0);
391340
document.querySelector("#run-btn img").style.transform = `rotate(${runButtonRotationDegree + 180}deg)`;
392341

393-
// Always apply the code to the preview, regardless of validation result
342+
// Apply the code to the preview via LessonEngine
394343
lessonEngine.applyUserCode(userCode, true);
395344

396-
// Backup code in local storage
397-
state.userCode.set(state.currentLessonIndex, userCode);
398-
localStorage.setItem("codeCrispies.userCode", JSON.stringify(Array.from(state.userCode.entries())));
399-
400-
const validationResult = validateUserCode(userCode, lesson);
345+
// Validate code using LessonEngine
346+
const validationResult = lessonEngine.validateCode();
401347

402348
// Add validation indicators based on validCases count if available
403349
if (validationResult.validCases) {
@@ -412,18 +358,10 @@ function runCode() {
412358
}
413359

414360
if (validationResult.isValid) {
415-
// Mark lesson as completed
416-
const moduleProgress = state.userProgress[state.currentModule.id];
417-
if (!moduleProgress.completed.includes(state.currentLessonIndex)) {
418-
moduleProgress.completed.push(state.currentLessonIndex);
419-
saveUserProgress();
420-
updateModuleSelectorButtonProgress();
421-
}
422-
423361
// Show success feedback with visual indicators
424362
showFeedback(true, validationResult.message || "Great job! Your code works correctly.");
425363

426-
// Add this block to update the Run button to Re-run
364+
// Update the Run button to Re-run
427365
elements.runBtn.innerHTML = '<img src="./gear.svg" />Re-run';
428366
elements.runBtn.classList.add("re-run");
429367

@@ -441,11 +379,11 @@ function runCode() {
441379
elements.nextBtn.classList.add("success");
442380
elements.taskInstruction.classList.add("success-instruction");
443381

444-
// Enable the next button if not already on the last lesson
445-
if (state.currentLessonIndex < state.currentModule.lessons.length - 1) {
446-
elements.nextBtn.disabled = false;
447-
elements.nextBtn.classList.remove("btn-disabled");
448-
}
382+
// Update navigation buttons
383+
updateNavigationButtons();
384+
385+
// Update progress indicator
386+
updateModuleSelectorButtonProgress();
449387
} else {
450388
// Reset any success indicators
451389
resetSuccessIndicators();
@@ -459,8 +397,11 @@ function runCode() {
459397
function showModuleSelector() {
460398
elements.modalTitle.textContent = "Select a Module";
461399

400+
const engineState = lessonEngine.getCurrentState();
401+
const modules = lessonEngine.modules;
402+
462403
// Create module buttons
463-
const moduleButtons = state.modules.map((module) => {
404+
const moduleButtons = modules.map((module) => {
464405
const button = document.createElement("button");
465406
button.classList.add("btn", "module-button");
466407
button.style.display = "block";
@@ -469,9 +410,8 @@ function showModuleSelector() {
469410
button.style.padding = "15px";
470411
button.style.textAlign = "left";
471412

472-
// Add completion status
473-
const progress = state.userProgress[module.id];
474-
const completedCount = progress ? progress.completed.length : 0;
413+
// Add completion status using LessonEngine
414+
const completedCount = lessonEngine.userProgress[module.id]?.completed.length || 0;
475415
const totalLessons = module.lessons.length;
476416
const percentComplete = Math.round((completedCount / totalLessons) * 100);
477417

0 commit comments

Comments
 (0)