import { LessonEngine } from "./impl/LessonEngine.js"; import { CodeEditor } from "./impl/CodeEditor.js"; import { renderLesson, renderModuleList, renderLevelIndicator, updateActiveLessonInSidebar } from "./helpers/renderer.js"; import { loadModules } from "./config/lessons.js"; // Simplified state - LessonEngine now manages lesson state and progress const state = { userSettings: { disableFeedbackErrors: false }, showExpected: false }; // DOM elements - updated for new layout const elements = { // Header menuBtn: document.getElementById("menu-btn"), helpBtn: document.getElementById("help-btn"), // Left panel modulePill: document.getElementById("module-pill"), 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"), 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"), // 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") }; // Initialize the lesson engine - now the single source of truth const lessonEngine = new LessonEngine(); // 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 = "Hide Expected"; elements.showExpectedBtn.classList.add("btn-primary"); } else { elements.expectedOverlay.classList.remove("visible"); elements.showExpectedBtn.textContent = "Show Expected"; elements.showExpectedBtn.classList.remove("btn-primary"); } } // ================= 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 = `${stats.percentComplete}% Complete (${stats.totalCompleted}/${stats.totalLessons})`; } // ================= 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)); } // ================= MODULE INITIALIZATION ================= async function initializeModules() { try { const modules = await loadModules(); lessonEngine.setModules(modules); // Use the new renderModuleList function with both callbacks renderModuleList(elements.moduleList, modules, selectModule, selectLesson); // 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(); } catch (error) { console.error("Failed to load modules:", error); elements.lessonDescription.textContent = "Failed to load modules. Please refresh the page."; } } // ================= 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: "Enter Tailwind classes (e.g., bg-blue-500 text-white p-4)", label: "Tailwind Classes", cmMode: "css" }, css: { placeholder: "Enter your CSS code here...", label: "CSS Editor", cmMode: "css" } }; 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"; // Update UI based on mode updateEditorForMode(mode); // Update module pill with category name if (elements.modulePill && engineState.module) { elements.modulePill.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 = "Show Expected"; 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.innerHTML = 'Re-run'; // Add completion badge if not present if (!document.querySelector(".completion-badge")) { const badge = document.createElement("span"); badge.className = "completion-badge"; badge.textContent = "Completed"; elements.lessonTitle.appendChild(badge); } } else { elements.runBtn.innerHTML = '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(); } // ================= 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); } 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 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() : ""; // 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); // Validate code using LessonEngine const validationResult = lessonEngine.validateCode(); if (validationResult.isValid) { // Show success hint showSuccessHint(validationResult.message || "CRISPY! ٩(◕‿◕)۶ Your code works correctly."); // Update Run button elements.runBtn.innerHTML = 'Re-run'; elements.runBtn.classList.add("success"); // Add completion badge if (!document.querySelector(".completion-badge")) { const badge = document.createElement("span"); badge.className = "completion-badge"; badge.textContent = "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 elements.previewWrapper?.classList.add("matched"); setTimeout(() => { elements.previewWrapper?.classList.remove("matched"); }, 2500); updateNavigationButtons(); updateProgressDisplay(); } 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 || "Keep trying!", 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(); } // ================= 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() { loadUserSettings(); // Initialize CodeMirror editor initCodeEditor(); // Load modules after editor is ready initializeModules().catch(console.error); // Sidebar controls elements.menuBtn.addEventListener("click", openSidebar); elements.closeSidebar.addEventListener("click", closeSidebar); elements.sidebarBackdrop.addEventListener("click", closeSidebar); // Expected result toggle elements.showExpectedBtn.addEventListener("click", toggleExpectedResult); // Navigation elements.prevBtn.addEventListener("click", prevLesson); elements.nextBtn.addEventListener("click", nextLesson); 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", resetCode); // Dialogs elements.helpBtn.addEventListener("click", showHelp); elements.helpDialogClose.addEventListener("click", closeHelpDialog); elements.resetBtn.addEventListener("click", showResetConfirmation); elements.resetDialogClose.addEventListener("click", closeResetDialog); elements.cancelReset.addEventListener("click", closeResetDialog); elements.confirmReset.addEventListener("click", handleResetConfirm); // 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();