import { LessonEngine } from "./impl/LessonEngine.js"; import { CodeEditor } from "./impl/CodeEditor.js"; import { renderLesson, renderModuleList, renderLevelIndicator, updateActiveLessonInSidebar, renderPathList } from "./helpers/renderer.js"; import { loadModules, loadLearningPaths } from "./config/lessons.js"; import { initI18n, t, getLanguage, setLanguage, applyTranslations } from "./i18n.js"; import { PathManager } from "./impl/PathManager.js"; // Simplified state - LessonEngine now manages lesson state and progress const state = { userSettings: { disableFeedbackErrors: false, skipResetCodeConfirmation: false }, showExpected: false }; // DOM elements - updated for new layout const elements = { // Header menuBtn: document.getElementById("menu-btn"), logoLink: document.getElementById("logo-link"), langSelect: document.getElementById("lang-select"), helpBtn: document.getElementById("help-btn"), pathIndicator: document.getElementById("path-indicator"), // Left panel instructionsSection: document.querySelector(".instructions"), editorSection: document.querySelector(".editor-section"), modulePill: document.getElementById("module-pill"), moduleName: document.querySelector(".module-name"), lessonTitle: document.getElementById("lesson-title"), lessonDescription: document.getElementById("lesson-description"), taskInstruction: document.getElementById("task-instruction"), codeInput: document.getElementById("code-input"), runBtn: document.getElementById("run-btn"), undoBtn: document.getElementById("undo-btn"), redoBtn: document.getElementById("redo-btn"), resetCodeBtn: document.getElementById("reset-code-btn"), hintArea: document.getElementById("hint-area"), editorContent: document.querySelector(".editor-content"), codeEditor: document.querySelector(".code-editor"), // Right panel previewArea: document.getElementById("preview-area"), showExpectedBtn: document.getElementById("show-expected-btn"), expectedOverlay: document.getElementById("expected-overlay"), previewWrapper: document.querySelector(".preview-wrapper"), prevBtn: document.getElementById("prev-btn"), nextBtn: document.getElementById("next-btn"), nextInPathBtn: document.getElementById("next-in-path-btn"), levelIndicator: document.getElementById("level-indicator"), // Sidebar sidebarDrawer: document.getElementById("sidebar-drawer"), sidebarBackdrop: document.getElementById("sidebar-backdrop"), closeSidebar: document.getElementById("close-sidebar"), moduleList: document.getElementById("module-list"), progressFill: document.getElementById("progress-fill"), progressText: document.getElementById("progress-text"), resetBtn: document.getElementById("reset-btn"), disableFeedbackToggle: document.getElementById("disable-feedback-toggle"), viewPathsBtn: document.getElementById("view-paths-btn"), pathProgressDisplay: document.getElementById("path-progress-display"), pathProgressFill: document.getElementById("path-progress-fill"), // Dialogs helpDialog: document.getElementById("help-dialog"), helpDialogClose: document.getElementById("help-dialog-close"), resetDialog: document.getElementById("reset-dialog"), resetDialogClose: document.getElementById("reset-dialog-close"), cancelReset: document.getElementById("cancel-reset"), confirmReset: document.getElementById("confirm-reset"), resetCodeDialog: document.getElementById("reset-code-dialog"), resetCodeDialogClose: document.getElementById("reset-code-dialog-close"), cancelResetCode: document.getElementById("cancel-reset-code"), confirmResetCode: document.getElementById("confirm-reset-code"), resetCodeDontShow: document.getElementById("reset-code-dont-show"), pathsDialog: document.getElementById("paths-dialog"), pathsDialogClose: document.getElementById("paths-dialog-close"), pathsList: document.getElementById("paths-list"), pathCompletionDialog: document.getElementById("path-completion-dialog"), pathCompletionDialogClose: document.getElementById("path-completion-dialog-close"), completionLessonsCount: document.getElementById("completion-lessons-count"), completionTimeTaken: document.getElementById("completion-time-taken"), nextPathSuggestion: document.getElementById("next-path-suggestion"), suggestedPathTitle: document.getElementById("suggested-path-title"), suggestedPathGoal: document.getElementById("suggested-path-goal"), startSuggestedPathBtn: document.getElementById("start-suggested-path-btn"), viewAllPathsFromCompletion: document.getElementById("view-all-paths-from-completion"), closeCompletionDialog: document.getElementById("close-completion-dialog") }; // Initialize the lesson engine - now the single source of truth const lessonEngine = new LessonEngine(); // Initialize the path manager - handles learning path state and progress const pathManager = new PathManager(); // Code editor instance (initialized later) let codeEditor = null; let currentMode = "css"; // ================= SIDEBAR FUNCTIONS ================= // Track element that opened sidebar for focus return let sidebarTrigger = null; function openSidebar() { // Store trigger element for focus return sidebarTrigger = document.activeElement; elements.sidebarDrawer.classList.add("open"); elements.sidebarBackdrop.classList.add("visible"); // Move focus to close button for keyboard users elements.closeSidebar.focus(); } function closeSidebar() { elements.sidebarDrawer.classList.remove("open"); elements.sidebarBackdrop.classList.remove("visible"); // Return focus to trigger element if (sidebarTrigger && typeof sidebarTrigger.focus === "function") { sidebarTrigger.focus(); sidebarTrigger = null; } } // ================= EXPECTED RESULT TOGGLE ================= function toggleExpectedResult() { state.showExpected = !state.showExpected; if (state.showExpected) { elements.expectedOverlay.classList.add("visible"); elements.showExpectedBtn.textContent = t("hideExpected"); elements.showExpectedBtn.classList.add("btn-primary"); } else { elements.expectedOverlay.classList.remove("visible"); elements.showExpectedBtn.textContent = t("showExpected"); elements.showExpectedBtn.classList.remove("btn-primary"); } } // ================= LANGUAGE TOGGLE ================= function changeLanguage(newLang) { // Add transition class before any updates elements.editorSection?.classList.add("transitioning"); setLanguage(newLang); applyTranslations(); // Reload lessons in new language const engineState = lessonEngine.getCurrentState(); const currentModuleId = engineState.module?.id; const currentLessonIndex = engineState.lessonIndex; const modules = loadModules(newLang); lessonEngine.setModules(modules); // Reload learning paths in new language const learningPaths = loadLearningPaths(newLang); pathManager.setPaths(learningPaths); renderModuleList(elements.moduleList, modules, selectModule, selectLesson, pathManager); // Restore position in current module/lesson if (currentModuleId) { lessonEngine.setModuleById(currentModuleId); lessonEngine.setLessonByIndex(currentLessonIndex); loadCurrentLesson(); } updateProgressDisplay(); // Remove transition class after all updates requestAnimationFrame(() => { elements.editorSection?.classList.remove("transitioning"); }); } // ================= HINT SYSTEM ================= function showHint(message, step, total, isSuccess = false) { const hintClass = isSuccess ? "hint hint-success" : "hint"; elements.hintArea.innerHTML = `
${step}/${total} ${message}
`; } function clearHint() { elements.hintArea.innerHTML = ""; } function showSuccessHint(message) { elements.hintArea.innerHTML = `
${message}
`; } // ================= PROGRESS DISPLAY ================= function updateProgressDisplay() { const stats = lessonEngine.getProgressStats(); elements.progressFill.style.width = `${stats.percentComplete}%`; elements.progressText.textContent = t("progressText", { percent: stats.percentComplete, completed: stats.totalCompleted, total: stats.totalLessons }); } function updatePathProgressDisplay() { const activePath = pathManager.getActivePath(); if (activePath && elements.pathProgressDisplay) { // Show path progress section elements.pathProgressDisplay.style.display = "block"; // Get path progress data const pathProgress = pathManager.getPathProgress(activePath.id); // Update path name const pathNameEl = elements.pathProgressDisplay.querySelector(".path-progress-name"); if (pathNameEl) { pathNameEl.textContent = activePath.title; } // Update path stats const pathStatsEl = elements.pathProgressDisplay.querySelector(".path-progress-stats"); if (pathStatsEl && pathProgress) { pathStatsEl.textContent = `${pathProgress.completedCount} / ${pathProgress.totalLessons} ${t("lessons")}`; } // Update progress bar if (elements.pathProgressFill && pathProgress) { elements.pathProgressFill.style.width = `${pathProgress.percentComplete}%`; } } else if (elements.pathProgressDisplay) { // Hide path progress section when no active path elements.pathProgressDisplay.style.display = "none"; } } // ================= USER SETTINGS ================= function loadUserSettings() { const savedSettings = localStorage.getItem("codeCrispies.settings"); if (savedSettings) { try { const settings = JSON.parse(savedSettings); state.userSettings = { ...state.userSettings, ...settings }; elements.disableFeedbackToggle.checked = !state.userSettings.disableFeedbackErrors; } catch (e) { console.error("Error loading user settings:", e); } } } function saveUserSettings() { localStorage.setItem("codeCrispies.settings", JSON.stringify(state.userSettings)); } // ================= LESSON CACHE ================= let cachedUserCode = null; function restoreLessonCache() { try { const cached = localStorage.getItem("codeCrispies.lessonCache"); if (cached) { const data = JSON.parse(cached); if (data.moduleTitle && elements.moduleName) { elements.moduleName.textContent = data.moduleTitle; // Remove data-i18n so applyTranslations won't overwrite elements.moduleName.removeAttribute("data-i18n"); } if (data.lessonTitle && elements.lessonTitle) { elements.lessonTitle.textContent = data.lessonTitle; elements.lessonTitle.removeAttribute("data-i18n"); } if (data.lessonDescription && elements.lessonDescription) { elements.lessonDescription.innerHTML = data.lessonDescription; } if (data.taskInstruction && elements.taskInstruction) { elements.taskInstruction.innerHTML = data.taskInstruction; } if (data.levelIndicator && elements.levelIndicator) { elements.levelIndicator.innerHTML = data.levelIndicator; } // Store userCode to apply after editor init if (data.userCode) { cachedUserCode = data.userCode; } } } catch (e) { // Ignore cache errors } } // ================= MODULE INITIALIZATION ================= let loadingTimeout = null; function showLoadingFallback() { // Only show if no lesson is loaded yet if (!elements.lessonTitle.textContent) { elements.lessonDescription.innerHTML = `

${t("loadingFallbackText")}

`; } } function clearLoadingTimeout() { if (loadingTimeout) { clearTimeout(loadingTimeout); loadingTimeout = null; } } function initializeModules() { try { const currentLang = getLanguage(); // Load modules const modules = loadModules(currentLang); lessonEngine.setModules(modules); // Load learning paths and connect to PathManager const learningPaths = loadLearningPaths(currentLang); pathManager.setPaths(learningPaths); // Connect PathManager to LessonEngine lessonEngine.setPathManager(pathManager); // Use the new renderModuleList function with both callbacks renderModuleList(elements.moduleList, modules, selectModule, selectLesson, pathManager); // Load saved progress and select appropriate module const progressData = lessonEngine.loadUserProgress(); const lastModuleId = progressData?.lastModuleId; if (lastModuleId && modules.find((m) => m.id === lastModuleId)) { selectModule(lastModuleId); } else if (modules.length > 0) { selectModule(modules[0].id); } updateProgressDisplay(); updatePathIndicator(); updatePathProgressDisplay(); clearLoadingTimeout(); } catch (error) { console.error("Failed to load modules:", error); showLoadingFallback(); } } // ================= MODULE/LESSON SELECTION ================= function selectModule(moduleId) { const success = lessonEngine.setModuleById(moduleId); if (!success) return; // Update module list UI to highlight the active module const moduleItems = elements.moduleList.querySelectorAll(".module-header"); moduleItems.forEach((item) => { item.classList.remove("active"); if (item.dataset.moduleId === moduleId) { item.classList.add("active"); } }); loadCurrentLesson(); resetSuccessIndicators(); // Close sidebar after selection on mobile if (window.innerWidth <= 768) { closeSidebar(); } } function selectLesson(moduleId, lessonIndex) { const currentState = lessonEngine.getCurrentState(); if (!currentState.module || currentState.module.id !== moduleId) { lessonEngine.setModuleById(moduleId); } lessonEngine.setLessonByIndex(lessonIndex); loadCurrentLesson(); // Close sidebar after selection on mobile if (window.innerWidth <= 768) { closeSidebar(); } } // ================= LESSON LOADING ================= function resetSuccessIndicators() { elements.codeEditor.classList.remove("success-highlight"); elements.lessonTitle.classList.remove("success-text"); elements.nextBtn.classList.remove("success"); elements.taskInstruction.classList.remove("success-instruction"); elements.runBtn.classList.remove("success"); elements.previewWrapper?.classList.remove("matched"); } function updateEditorForMode(mode) { const editorLabel = document.querySelector(".editor-label"); const modeConfig = { html: { placeholder: "Type HTML here... Try: nav>ul>li*3 then press Tab", label: "HTML Editor", cmMode: "html" }, tailwind: { placeholder: t("tailwindPlaceholder"), label: "Tailwind Classes", cmMode: "css" }, css: { placeholder: "Enter your CSS code here...", label: "CSS Editor", cmMode: "css" }, playground: { placeholder: "\n\n", label: "HTML & CSS", cmMode: "html" } }; const config = modeConfig[mode] || modeConfig.css; if (editorLabel) editorLabel.textContent = config.label; // Update CodeMirror mode if needed if (codeEditor && currentMode !== config.cmMode) { currentMode = config.cmMode; codeEditor.setMode(config.cmMode); } } function loadCurrentLesson() { const engineState = lessonEngine.getCurrentState(); if (!engineState.module || !engineState.lesson) { return; } const lesson = engineState.lesson; const mode = lesson.mode || engineState.module?.mode || "css"; const isPlayground = lesson.mode === "playground"; // Handle playground mode - hide instructions, full height editor if (isPlayground) { elements.instructionsSection?.classList.add("hidden"); elements.editorSection?.classList.add("playground-mode"); } else { elements.instructionsSection?.classList.remove("hidden"); elements.editorSection?.classList.remove("playground-mode"); } // Add transition class for smooth content swap elements.editorSection?.classList.add("transitioning"); // Update UI based on mode updateEditorForMode(mode); // Update module name in pill if (elements.moduleName && engineState.module) { elements.moduleName.textContent = engineState.module.title; } // Reset any success indicators resetSuccessIndicators(); // Clear hints clearHint(); // Hide expected overlay state.showExpected = false; elements.expectedOverlay.classList.remove("visible"); elements.showExpectedBtn.textContent = t("showExpected"); elements.showExpectedBtn.classList.remove("btn-primary"); // Update UI renderLesson( elements.lessonTitle, elements.lessonDescription, elements.taskInstruction, elements.previewArea, null, // editorPrefix no longer used null, // codeInput no longer used (using CodeMirror) null, // editorSuffix no longer used lesson ); // Set user code in CodeMirror if (codeEditor) { codeEditor.setValue(engineState.userCode); } // Update Run button text based on completion status if (engineState.isCompleted) { elements.runBtn.querySelector("span").textContent = t("rerun"); // Add completion badge if not present if (!document.querySelector(".completion-badge")) { const badge = document.createElement("span"); badge.className = "completion-badge"; badge.textContent = t("completed"); elements.lessonTitle.appendChild(badge); } } else { elements.runBtn.querySelector("span").textContent = t("run"); // Remove completion badge if exists const badge = document.querySelector(".completion-badge"); if (badge) badge.remove(); } // Update level indicator renderLevelIndicator(elements.levelIndicator, engineState.lessonIndex + 1, engineState.totalLessons); // Update active lesson in sidebar updateActiveLessonInSidebar(engineState.module.id, engineState.lessonIndex); // Update navigation buttons updateNavigationButtons(); // Update progress display updateProgressDisplay(); // Focus on the code editor if (codeEditor) { codeEditor.focus(); } // Render the expected/solution preview lessonEngine.renderExpectedPreview(); // Remove transition class after content is updated requestAnimationFrame(() => { elements.editorSection?.classList.remove("transitioning"); }); // Cache lesson display data for instant restore on reload try { localStorage.setItem( "codeCrispies.lessonCache", JSON.stringify({ moduleTitle: engineState.module?.title, lessonTitle: lesson.title, lessonDescription: lesson.description, taskInstruction: lesson.task, levelIndicator: elements.levelIndicator?.innerHTML, userCode: engineState.userCode, mode: mode }) ); } catch (e) { // Ignore storage errors } } // ================= LIVE PREVIEW ================= let previewTimer = null; function handleEditorChange(code) { if (previewTimer) { clearTimeout(previewTimer); } previewTimer = setTimeout(() => { runCode(); }, 800); } // ================= NAVIGATION ================= function updateNavigationButtons() { const engineState = lessonEngine.getCurrentState(); elements.prevBtn.disabled = !engineState.canGoPrev; elements.nextBtn.disabled = !engineState.canGoNext; elements.prevBtn.classList.toggle("btn-disabled", !engineState.canGoPrev); elements.nextBtn.classList.toggle("btn-disabled", !engineState.canGoNext); // Show "Next in Path" button if a path is active const pathManager = lessonEngine.pathManager; if (pathManager) { const activePath = pathManager.getActivePath(); const hasActivePath = activePath !== null; if (hasActivePath) { const nextLesson = pathManager.getNextLesson(activePath.id); const hasNextInPath = nextLesson !== null; // Show button only if there's a next lesson in the path elements.nextInPathBtn.style.display = hasNextInPath ? "" : "none"; elements.nextInPathBtn.disabled = !hasNextInPath; } else { elements.nextInPathBtn.style.display = "none"; } } else { // PathManager not initialized yet - hide the button elements.nextInPathBtn.style.display = "none"; } } function nextLesson() { const prevModuleId = lessonEngine.getCurrentState().module?.id; const success = lessonEngine.nextLesson(); if (success) { const newModuleId = lessonEngine.getCurrentState().module?.id; if (newModuleId !== prevModuleId) { updateModuleHighlight(newModuleId); } loadCurrentLesson(); } } function prevLesson() { const prevModuleId = lessonEngine.getCurrentState().module?.id; const success = lessonEngine.previousLesson(); if (success) { const newModuleId = lessonEngine.getCurrentState().module?.id; if (newModuleId !== prevModuleId) { updateModuleHighlight(newModuleId); } loadCurrentLesson(); } } function nextLessonInPath() { // Check if PathManager is available (will be initialized in Phase 4) const pathManager = lessonEngine.pathManager; if (!pathManager) return; const activePath = pathManager.getActivePath(); if (!activePath) return; // Get the next incomplete lesson in the path const nextLesson = pathManager.getNextLesson(activePath.id); if (!nextLesson) return; // Navigate to the next lesson in the path const prevModuleId = lessonEngine.getCurrentState().module?.id; const success = lessonEngine.setModuleById(nextLesson.moduleId, nextLesson.lessonIndex); if (success) { const newModuleId = lessonEngine.getCurrentState().module?.id; if (newModuleId !== prevModuleId) { updateModuleHighlight(newModuleId); } loadCurrentLesson(); } } function updateModuleHighlight(moduleId) { const moduleItems = elements.moduleList.querySelectorAll(".module-header"); moduleItems.forEach((item) => { item.classList.remove("active"); if (item.dataset.moduleId === moduleId) { item.classList.add("active"); } }); } // ================= CODE EXECUTION ================= function resetCode() { // Reset editor to initial code for current lesson lessonEngine.reset(); const engineState = lessonEngine.getCurrentState(); if (codeEditor && engineState.lesson) { codeEditor.setValue(engineState.lesson.initialCode || ""); } // Clear hints and success indicators clearHint(); resetSuccessIndicators(); } function runCode() { const userCode = codeEditor ? codeEditor.getValue() : ""; const engineState = lessonEngine.getCurrentState(); const isPlayground = engineState.lesson?.mode === "playground"; // Rotate the Run button icon const runButtonImg = document.querySelector("#run-btn img"); if (runButtonImg) { const currentRotation = parseInt(runButtonImg.style.transform?.match(/\d+/)?.[0] || "0"); runButtonImg.style.transform = `rotate(${currentRotation + 180}deg)`; } // Apply the code to the preview via LessonEngine lessonEngine.applyUserCode(userCode, true); // Skip validation for playground mode if (isPlayground) { return; } // Validate code using LessonEngine const validationResult = lessonEngine.validateCode(); if (validationResult.isValid) { // Show success hint showSuccessHint(validationResult.message || t("successMessage")); // Update Run button elements.runBtn.querySelector("span").textContent = t("rerun"); elements.runBtn.classList.add("success"); // Add completion badge if (!document.querySelector(".completion-badge")) { const badge = document.createElement("span"); badge.className = "completion-badge"; badge.textContent = t("completed"); elements.lessonTitle.appendChild(badge); } // Add success visual indicators elements.codeEditor.classList.add("success-highlight"); elements.lessonTitle.classList.add("success-text"); elements.nextBtn.classList.add("success"); elements.taskInstruction.classList.add("success-instruction"); // Show match animation (DVD-style bouncing) elements.previewWrapper?.classList.add("matched"); setTimeout(() => { elements.previewWrapper?.classList.remove("matched"); }, 10000); updateNavigationButtons(); updateProgressDisplay(); updatePathIndicator(); updatePathProgressDisplay(); // Check if path is complete and show celebration checkPathCompletion(); } else { // Reset success indicators resetSuccessIndicators(); // Show hint with step progress const step = validationResult.validCases + 1; const total = validationResult.totalCases; // Only show hints if enabled if (!state.userSettings.disableFeedbackErrors) { showHint(validationResult.message || t("keepTrying"), step, total); } } } // ================= DIALOGS ================= function showHelp() { elements.helpDialog.showModal(); } function closeHelpDialog() { elements.helpDialog.close(); } function showResetConfirmation() { elements.resetDialog.showModal(); } function closeResetDialog() { elements.resetDialog.close(); } function handleResetConfirm() { lessonEngine.clearProgress(); closeResetDialog(); closeSidebar(); // Reload first module const modules = lessonEngine.modules; if (modules.length > 0) { selectModule(modules[0].id); } updateProgressDisplay(); } function showResetCodeConfirmation() { // Reset the checkbox state each time dialog is shown elements.resetCodeDontShow.checked = false; elements.resetCodeDialog.showModal(); } function closeResetCodeDialog() { elements.resetCodeDialog.close(); } function handleResetCodeConfirm() { // Save preference if checkbox is checked if (elements.resetCodeDontShow.checked) { state.userSettings.skipResetCodeConfirmation = true; saveUserSettings(); } closeResetCodeDialog(); resetCode(); } function handleResetCodeClick() { if (state.userSettings.skipResetCodeConfirmation) { resetCode(); } else { showResetCodeConfirmation(); } } // ================= LEARNING PATHS ================= function openPathsDialog() { // Render the path list const paths = pathManager.paths; if (paths && paths.length > 0) { renderPathList(elements.pathsList, paths, pathManager); } elements.pathsDialog.showModal(); } function closePathsDialog() { elements.pathsDialog.close(); } function handlePathAction(pathId) { const activePath = pathManager.getActivePath(); const pathProgress = pathManager.getPathProgress(pathId); // Determine action based on current state if (pathProgress && pathProgress.isComplete) { // Review completed path - restart it pathManager.startPath(pathId); } else if (activePath && activePath.id === pathId) { // Continue active path - navigate to next lesson const nextLesson = pathManager.getNextLesson(pathId); if (nextLesson) { lessonEngine.setModuleById(nextLesson.moduleId, nextLesson.lessonIndex); loadCurrentLesson(); } } else if (pathProgress && pathProgress.isStarted) { // Resume paused path pathManager.resumePath(pathId); } else { // Start new path pathManager.startPath(pathId); } // Update UI updatePathIndicator(); updatePathProgressDisplay(); updateNavigationButtons(); // Refresh module list to update path highlighting const modules = lessonEngine.getModules(); renderModuleList(elements.moduleList, modules, selectModule, selectLesson, pathManager); updateActiveLessonInSidebar(elements.moduleList, lessonEngine.getCurrentState()); // Close dialog and sidebar closePathsDialog(); if (window.innerWidth <= 768) { closeSidebar(); } // If starting/resuming, navigate to the next incomplete lesson if (!pathProgress || !pathProgress.isComplete) { const nextLesson = pathManager.getNextLesson(pathId); if (nextLesson) { const prevModuleId = lessonEngine.getCurrentState().module?.id; lessonEngine.setModuleById(nextLesson.moduleId, nextLesson.lessonIndex); if (nextLesson.moduleId !== prevModuleId) { updateModuleHighlight(nextLesson.moduleId); } loadCurrentLesson(); } } } function updatePathIndicator() { const activePath = pathManager.getActivePath(); if (activePath) { // Show path indicator const pathProgress = pathManager.getPathProgress(activePath.id); const pathNameSpan = elements.pathIndicator.querySelector(".path-indicator-name"); const pathProgressSpan = elements.pathIndicator.querySelector(".path-indicator-progress"); if (pathNameSpan) { pathNameSpan.textContent = activePath.title; } if (pathProgressSpan && pathProgress) { pathProgressSpan.textContent = `${pathProgress.percentComplete}%`; } elements.pathIndicator.style.display = ""; } else { // Hide path indicator elements.pathIndicator.style.display = "none"; } } function pauseActivePath() { pathManager.pausePath(); updatePathIndicator(); updatePathProgressDisplay(); updateNavigationButtons(); // Refresh module list to update path highlighting const modules = lessonEngine.getModules(); renderModuleList(elements.moduleList, modules, selectModule, selectLesson, pathManager); updateActiveLessonInSidebar(elements.moduleList, lessonEngine.getCurrentState()); } /** * Format duration in milliseconds to human-readable time string * @param {number} milliseconds - Duration in milliseconds * @returns {string} Formatted time string (e.g., "45 min", "2h 30m") */ function formatTimeDuration(milliseconds) { const totalMinutes = Math.floor(milliseconds / (1000 * 60)); if (totalMinutes < 60) { return `${totalMinutes} min`; } const hours = Math.floor(totalMinutes / 60); const minutes = totalMinutes % 60; if (minutes === 0) { return `${hours}h`; } return `${hours}h ${minutes}m`; } /** * Get recommended next path based on completed path and prerequisites * @param {string} completedPathId - ID of the completed path * @returns {Object|null} Recommended path object or null */ function getRecommendedNextPath(completedPathId) { const allPaths = pathManager.paths; const completedPath = allPaths.find(p => p.id === completedPathId); if (!completedPath) return null; // Find paths that list the completed path as a prerequisite const pathsWithCompletedAsPrereq = allPaths.filter(path => { return path.prerequisites && path.prerequisites.includes(completedPathId); }); // Return first path that has the completed path as prerequisite and is not yet completed for (const path of pathsWithCompletedAsPrereq) { const progress = pathManager.getPathProgress(path.id); if (!progress || !progress.isComplete) { return path; } } // If no paths have it as prerequisite, suggest next difficulty level const difficultyOrder = ["beginner", "intermediate", "advanced"]; const currentDifficultyIndex = difficultyOrder.indexOf(completedPath.difficulty); if (currentDifficultyIndex < difficultyOrder.length - 1) { const nextDifficulty = difficultyOrder[currentDifficultyIndex + 1]; const sameDifficultyPaths = allPaths.filter(path => { const progress = pathManager.getPathProgress(path.id); return path.difficulty === nextDifficulty && (!progress || !progress.isComplete); }); if (sameDifficultyPaths.length > 0) { return sameDifficultyPaths[0]; } } // Otherwise, suggest any incomplete path for (const path of allPaths) { const progress = pathManager.getPathProgress(path.id); if (path.id !== completedPathId && (!progress || !progress.isComplete)) { return path; } } return null; } /** * Show path completion celebration dialog * @param {string} pathId - ID of the completed path */ function showPathCompletionDialog(pathId) { const path = pathManager.paths.find(p => p.id === pathId); if (!path) return; const pathProgress = pathManager.getPathProgress(pathId); if (!pathProgress) return; // Calculate time taken const startTime = new Date(pathProgress.startTimestamp).getTime(); const endTime = Date.now(); const timeTaken = endTime - startTime; // Update stats elements.completionLessonsCount.textContent = pathProgress.totalLessons; elements.completionTimeTaken.textContent = formatTimeDuration(timeTaken); // Get recommended next path const recommendedPath = getRecommendedNextPath(pathId); if (recommendedPath) { elements.suggestedPathTitle.textContent = recommendedPath.title; elements.suggestedPathGoal.textContent = recommendedPath.goal; elements.nextPathSuggestion.style.display = ""; // Store recommended path ID for the button handler elements.startSuggestedPathBtn.dataset.pathId = recommendedPath.id; } else { elements.nextPathSuggestion.style.display = "none"; } // Show the dialog elements.pathCompletionDialog.showModal(); } /** * Close path completion dialog */ function closePathCompletionDialog() { elements.pathCompletionDialog.close(); } /** * Check if path is complete after lesson completion and show celebration */ function checkPathCompletion() { const activePath = pathManager.getActivePath(); if (!activePath) return; const pathProgress = pathManager.getPathProgress(activePath.id); if (pathProgress && pathProgress.isComplete) { // Small delay to let UI update before showing dialog setTimeout(() => { showPathCompletionDialog(activePath.id); }, 500); } } // ================= INITIALIZATION ================= function initCodeEditor() { const container = elements.editorContent; if (!container) return; // Remove the textarea - CodeMirror will replace it const textarea = container.querySelector("textarea"); if (textarea) { textarea.remove(); } // Initialize CodeMirror codeEditor = new CodeEditor(container, { mode: currentMode, placeholder: "Type your code here...", onChange: handleEditorChange }); codeEditor.init(""); } function init() { // Initialize i18n before anything else initI18n(); loadUserSettings(); // Restore cached lesson content immediately to avoid "Loading..." flash restoreLessonCache(); // Initialize CodeMirror editor initCodeEditor(); // Set timeout to show fallback if loading takes too long loadingTimeout = setTimeout(showLoadingFallback, 3000); // Load modules after editor is ready initializeModules(); // Sidebar controls elements.menuBtn.addEventListener("click", openSidebar); elements.closeSidebar.addEventListener("click", closeSidebar); elements.sidebarBackdrop.addEventListener("click", closeSidebar); // Logo click - navigate to welcome elements.logoLink.addEventListener("click", (e) => { e.preventDefault(); lessonEngine.setModuleById("welcome"); loadCurrentLesson(); }); // Language select elements.langSelect.value = getLanguage(); elements.langSelect.addEventListener("change", (e) => changeLanguage(e.target.value)); // Expected result toggle elements.showExpectedBtn.addEventListener("click", toggleExpectedResult); // Navigation elements.prevBtn.addEventListener("click", prevLesson); elements.nextBtn.addEventListener("click", nextLesson); elements.nextInPathBtn.addEventListener("click", nextLessonInPath); elements.runBtn.addEventListener("click", runCode); // Editor tools elements.undoBtn.addEventListener("click", () => { if (codeEditor) codeEditor.undo(); }); elements.redoBtn.addEventListener("click", () => { if (codeEditor) codeEditor.redo(); }); elements.resetCodeBtn.addEventListener("click", handleResetCodeClick); // Dialogs elements.helpBtn.addEventListener("click", showHelp); elements.helpDialogClose.addEventListener("click", closeHelpDialog); elements.helpDialog.addEventListener("click", (e) => { if (e.target === elements.helpDialog) closeHelpDialog(); }); elements.resetBtn.addEventListener("click", showResetConfirmation); elements.resetDialogClose.addEventListener("click", closeResetDialog); elements.resetDialog.addEventListener("click", (e) => { if (e.target === elements.resetDialog) closeResetDialog(); }); elements.cancelReset.addEventListener("click", closeResetDialog); elements.confirmReset.addEventListener("click", handleResetConfirm); elements.resetCodeDialogClose.addEventListener("click", closeResetCodeDialog); elements.resetCodeDialog.addEventListener("click", (e) => { if (e.target === elements.resetCodeDialog) closeResetCodeDialog(); }); elements.cancelResetCode.addEventListener("click", closeResetCodeDialog); elements.confirmResetCode.addEventListener("click", handleResetCodeConfirm); // Learning Paths Dialog elements.viewPathsBtn.addEventListener("click", openPathsDialog); elements.pathIndicator.addEventListener("click", openPathsDialog); elements.pathsDialogClose.addEventListener("click", closePathsDialog); elements.pathsDialog.addEventListener("click", (e) => { if (e.target === elements.pathsDialog) closePathsDialog(); }); // Delegated event handler for path action buttons elements.pathsList.addEventListener("click", (e) => { const button = e.target.closest(".path-card-action"); if (button) { const card = button.closest(".path-card"); if (card && card.dataset.pathId) { handlePathAction(card.dataset.pathId); } } }); // Path Completion Dialog elements.pathCompletionDialogClose.addEventListener("click", closePathCompletionDialog); elements.pathCompletionDialog.addEventListener("click", (e) => { if (e.target === elements.pathCompletionDialog) closePathCompletionDialog(); }); elements.closeCompletionDialog.addEventListener("click", closePathCompletionDialog); elements.viewAllPathsFromCompletion.addEventListener("click", () => { closePathCompletionDialog(); openPathsDialog(); }); elements.startSuggestedPathBtn.addEventListener("click", () => { const pathId = elements.startSuggestedPathBtn.dataset.pathId; if (pathId) { closePathCompletionDialog(); handlePathAction(pathId); } }); // Settings elements.disableFeedbackToggle.addEventListener("change", (e) => { state.userSettings.disableFeedbackErrors = !e.target.checked; saveUserSettings(); }); // Click on editor content to focus CodeMirror elements.editorContent?.addEventListener("click", () => { if (codeEditor) codeEditor.focus(); }); // Keyboard shortcuts document.addEventListener("keydown", (e) => { // Ctrl+Enter to run code if (e.ctrlKey && e.key === "Enter") { runCode(); e.preventDefault(); } // Escape to close sidebar (dialogs handle Escape natively) if (e.key === "Escape") { closeSidebar(); } }); } // Start the application init();