/** * LessonEngine - Core class for managing lessons and applying/testing user code * Single source of truth for lesson state and progress */ import { validateUserCode } from "../helpers/validator.js"; export class LessonEngine { constructor() { this.currentLesson = null; this.userCode = ""; this.currentModule = null; this.currentLessonIndex = 0; this.lastRenderedCode = ""; // Track last applied code to prevent unnecessary re-renders this.modules = []; this.userProgress = {}; // Format: { moduleId: { completed: [0, 2, 3], current: 4 } } this.userCodeMap = new Map(); // Store user code for each lesson this.loadUserProgress(); } /** * Initialize with modules array * @param {Array} modules - Available modules */ setModules(modules) { this.modules = modules; this.loadUserCodeFromStorage(); } /** * Set the current module * @param {Object} module - The module object from the config */ setModule(module) { this.currentModule = module; this.currentLessonIndex = 0; // Load user progress for this module if (!this.userProgress[module.id]) { this.userProgress[module.id] = { completed: [], current: 0 }; } this.currentLessonIndex = this.userProgress[module.id].current || 0; if (module && module.lessons && module.lessons.length > 0) { this.setLesson(module.lessons[this.currentLessonIndex]); } this.saveUserProgress(); } /** * Set module by ID * @param {string} moduleId - The module ID * @returns {boolean} Whether the operation was successful */ setModuleById(moduleId) { const module = this.modules.find((m) => m.id === moduleId); if (!module) return false; this.setModule(module); return true; } /** * Set the current lesson * @param {Object} lesson - The lesson object from the config */ setLesson(lesson) { this.currentLesson = lesson; // Load saved user code for this lesson or use initial code const lessonKey = `${this.currentModule.id}-${this.currentLessonIndex}`; this.userCode = this.userCodeMap.get(lessonKey) || lesson.initialCode || ""; this.lastRenderedCode = ""; // Reset last rendered code this.renderPreview(); } /** * Set lesson by index within the current module * @param {number} index - The lesson index * @returns {boolean} Whether the operation was successful */ setLessonByIndex(index) { if (!this.currentModule || !this.currentModule.lessons) { return false; } if (index < 0 || index >= this.currentModule.lessons.length) { return false; } this.currentLessonIndex = index; this.setLesson(this.currentModule.lessons[index]); // Update progress this.userProgress[this.currentModule.id].current = index; this.saveUserProgress(); return true; } /** * Move to the next lesson (crosses module boundaries) * @returns {boolean} Whether the operation was successful */ nextLesson() { // Try next lesson in current module if (this.setLessonByIndex(this.currentLessonIndex + 1)) { return true; } // At end of module, try next module const currentModuleIndex = this.modules.findIndex((m) => m.id === this.currentModule?.id); if (currentModuleIndex >= 0 && currentModuleIndex < this.modules.length - 1) { const nextModule = this.modules[currentModuleIndex + 1]; this.setModule(nextModule); this.setLessonByIndex(0); // Start at first lesson return true; } return false; } /** * Move to the previous lesson (crosses module boundaries) * @returns {boolean} Whether the operation was successful */ previousLesson() { // Try previous lesson in current module if (this.setLessonByIndex(this.currentLessonIndex - 1)) { return true; } // At start of module, try previous module const currentModuleIndex = this.modules.findIndex((m) => m.id === this.currentModule?.id); if (currentModuleIndex > 0) { const prevModule = this.modules[currentModuleIndex - 1]; this.setModule(prevModule); // Go to last lesson of previous module const lastIndex = prevModule.lessons.length - 1; this.setLessonByIndex(lastIndex); return true; } return false; } /** * Apply user-written CSS to the preview area * @param {string} code - User CSS code * @param {boolean} forceUpdate - Force update the preview even if code hasn't changed */ applyUserCode(code, forceUpdate = false) { if (!this.currentLesson) return; this.userCode = code; // Save user code for this lesson const lessonKey = `${this.currentModule.id}-${this.currentLessonIndex}`; this.userCodeMap.set(lessonKey, code); this.saveUserCodeToStorage(); // Only re-render if code changed or forced update if (forceUpdate || this.lastRenderedCode !== code) { this.lastRenderedCode = code; this.renderPreview(); } } /** * Get the complete CSS by combining all parts * @returns {string} The complete CSS */ getCompleteCss() { if (!this.currentLesson) return ""; const { codePrefix, codeSuffix } = this.currentLesson; return ` ${codePrefix || ""} ${this.userCode || ""} ${codeSuffix || ""} `; } /** * Render the preview for the current lesson */ renderPreview() { if (!this.currentLesson) return; const mode = this.currentLesson.mode || this.currentModule?.mode || "css"; const { previewHTML, previewBaseCSS, previewContainer, sandboxCSS } = this.currentLesson; const iframe = document.createElement("iframe"); iframe.style.width = "100%"; iframe.style.height = "100%"; iframe.style.border = "none"; iframe.title = "Preview"; const container = document.getElementById(previewContainer || "preview-area"); container.innerHTML = ""; container.appendChild(iframe); const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; iframeDoc.open(); if (mode === "html") { // For HTML mode, user code IS the HTML content const userHtml = this.userCode || ""; iframeDoc.write(`
${userHtml} `); } else if (mode === "tailwind") { // For Tailwind mode, user code goes directly in HTML classes const htmlWithClasses = this.injectTailwindClasses(previewHTML, this.userCode); iframeDoc.write(` ${htmlWithClasses} `); } else { // Original CSS mode const userCssWithWrapper = this.getCompleteCss(); iframeDoc.write(` ${previewHTML} `); } iframeDoc.close(); } injectTailwindClasses(html, userClasses) { // Replace placeholder in HTML with user's Tailwind classes return html.replace(/{{USER_CLASSES}}/g, userClasses); } /** * Render the expected/solution preview for comparison */ renderExpectedPreview() { if (!this.currentLesson) return; // Use 'solution' property from lesson JSON (not 'solutionCode') const solutionCode = this.currentLesson.solution; if (!solutionCode) { // No solution code provided, hide the expected pane or show placeholder const expectedContainer = document.getElementById("preview-expected"); if (expectedContainer) { expectedContainer.innerHTML = '