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();