import { LessonEngine } from "./impl/LessonEngine.js"; import { renderLesson, renderModuleList, renderLevelIndicator, showFeedback } from "./helpers/renderer.js"; import { validateUserCode } from "./helpers/validator.js"; import { loadModules } from "./config/lessons.js"; // Main Application state const state = { currentModule: null, currentLessonIndex: 0, modules: [], userProgress: {}, // Format: { moduleId: { completed: [0, 2, 3], current: 4 } } userCodeBeforeValidation: "", // Track user code state before validation userSettings: { disableFeedbackErrors: false } }; // DOM elements const elements = { moduleList: document.querySelector(".module-list"), lessonTitle: document.getElementById("lesson-title"), lessonDescription: document.getElementById("lesson-description"), taskInstruction: document.getElementById("task-instruction"), previewArea: document.getElementById("preview-area"), editorPrefix: document.getElementById("editor-prefix"), codeInput: document.getElementById("code-input"), editorSuffix: document.getElementById("editor-suffix"), prevBtn: document.getElementById("prev-btn"), nextBtn: document.getElementById("next-btn"), runBtn: document.getElementById("run-btn"), levelIndicator: document.getElementById("level-indicator"), modalContainer: document.getElementById("modal-container"), modalTitle: document.getElementById("modal-title"), modalContent: document.getElementById("modal-content"), modalClose: document.getElementById("modal-close"), moduleSelectorBtn: document.getElementById("module-selector-btn"), resetBtn: document.getElementById("reset-btn"), helpBtn: document.getElementById("help-btn"), lessonContainer: document.querySelector(".lesson-container"), editorContent: document.querySelector(".editor-content"), codeEditor: document.querySelector(".code-editor"), validationIndicators: document.querySelector(".validation-indicators-container"), disableFeedbackToggle: document.getElementById("disable-feedback-toggle") }; // Initialize the lesson engine const lessonEngine = new LessonEngine(); // Load user progress from localStorage function loadUserProgress() { const savedProgress = localStorage.getItem("codeCrispies.Progress"); if (savedProgress) { state.userProgress = JSON.parse(savedProgress); } } // Save user progress to localStorage function saveUserProgress() { localStorage.setItem("codeCrispies.Progress", JSON.stringify(state.userProgress)); } function loadUserSettings() { const savedSettings = localStorage.getItem("codeCrispies.settings"); if (savedSettings) { try { const settings = JSON.parse(savedSettings); state.userSettings = { ...state.userSettings, ...settings }; // Apply saved settings to UI 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)); } function initFeedbackToggle() { elements.disableFeedbackToggle.addEventListener("change", (e) => { state.userSettings.disableFeedbackErrors = !e.target.checked; saveUserSettings(); }); } // Initialize the module list async function initializeModules() { try { state.modules = await loadModules(); renderModuleList(elements.moduleList, state.modules, selectModule); // Select the first module or the last one user was on const lastModuleId = localStorage.getItem("codeCrispies.lastModuleId"); if (lastModuleId && state.modules.find((m) => m.id === lastModuleId)) { selectModule(lastModuleId); } else if (state.modules.length > 0) { selectModule(state.modules[0].id); } // Update progress indicator on module selector button updateModuleSelectorButtonProgress(); } catch (error) { console.error("Failed to load modules:", error); elements.lessonDescription.textContent = "Failed to load modules. Please refresh the page."; } } // Update progress indicator on module selector button function updateModuleSelectorButtonProgress() { if (!state.modules.length) return; // Calculate overall progress across all modules let totalLessons = 0; let totalCompleted = 0; state.modules.forEach((module) => { totalLessons += module.lessons.length; const progress = state.userProgress[module.id]; if (progress && progress.completed) { totalCompleted += progress.completed.length; } }); const percentComplete = totalLessons > 0 ? Math.round((totalCompleted / totalLessons) * 100) : 0; // Create progress indicator const progressBar = document.createElement("div"); progressBar.className = "progress-indicator"; progressBar.style.cssText = ` position: absolute; bottom: 0; left: 0; height: 2px; width: ${percentComplete}%; background-color: var(--primary-color); border-radius: 0 3px 3px 0; `; // Add progress percentage text elements.moduleSelectorBtn.innerHTML = `Progress ${percentComplete}%`; elements.moduleSelectorBtn.style.position = "relative"; // Remove any existing progress bar before adding new one const existingBar = elements.moduleSelectorBtn.querySelector(".progress-indicator"); if (existingBar) { existingBar.remove(); } elements.moduleSelectorBtn.appendChild(progressBar); } // Select a module function selectModule(moduleId) { const selectedModule = state.modules.find((module) => module.id === moduleId); if (!selectedModule) return; state.currentModule = selectedModule; // Update module list UI const moduleItems = elements.moduleList.querySelectorAll(".module-list-item"); moduleItems.forEach((item) => { item.classList.remove("active"); if (item.dataset.moduleId === moduleId) { item.classList.add("active"); } }); // Load user progress for this module if (!state.userProgress[moduleId]) { state.userProgress[moduleId] = { completed: [], current: 0 }; } state.currentLessonIndex = state.userProgress[moduleId].current || 0; loadCurrentLesson(); // Save the last selected module localStorage.setItem("codeCrispies.lastModuleId", moduleId); // Reset any success indicators resetSuccessIndicators(); } // Reset success indicators 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("re-run"); } // Configure editor layout based on display type function resetEditorLayout(lesson) { elements.validationIndicators.innerHTML = ""; } // Load the current lesson function loadCurrentLesson() { if (!state.currentModule || !state.currentModule.lessons) { return; } // Make sure lesson index is in bounds if (state.currentLessonIndex >= state.currentModule.lessons.length) { state.currentLessonIndex = state.currentModule.lessons.length - 1; } else if (state.currentLessonIndex < 0) { state.currentLessonIndex = 0; } const lesson = state.currentModule.lessons[state.currentLessonIndex]; lessonEngine.setLesson(lesson); // Reset any success indicators resetSuccessIndicators(); // Update UI renderLesson( elements.lessonTitle, elements.lessonDescription, elements.taskInstruction, elements.previewArea, elements.editorPrefix, elements.codeInput, elements.editorSuffix, lesson ); // Configure editor layout based on lesson settings resetEditorLayout(lesson); // Update Run button text based on completion status const moduleProgress = state.userProgress[state.currentModule.id]; if (moduleProgress && moduleProgress.completed.includes(state.currentLessonIndex)) { elements.runBtn.innerHTML = 'Re-run'; // Add completion badge next to title if not already 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, state.currentLessonIndex + 1, state.currentModule.lessons.length); // Update navigation buttons updateNavigationButtons(); // Save current progress state.userProgress[state.currentModule.id].current = state.currentLessonIndex; saveUserProgress(); // Update progress indicator on module selector button updateModuleSelectorButtonProgress(); // Focus on the code editor by default elements.codeInput.focus(); // Store current code state.userCodeBeforeValidation = elements.codeInput.value; // Track live changes and update preview when the user pauses typing setupLivePreview(); } // Setup live preview functionality let previewTimer = null; function setupLivePreview() { // Clear previous event listener if any elements.codeInput.removeEventListener("input", handleUserInput); // Add new event listener elements.codeInput.addEventListener("input", handleUserInput); } // Handle user input with debounced preview updates function handleUserInput() { // Clear the previous timer if (previewTimer) { clearTimeout(previewTimer); } // Set a new timer for preview update after user stops typing previewTimer = setTimeout(() => { // Apply the code for preview without validation // lessonEngine.applyUserCode(elements.codeInput.value); runCode(); }, 800); // Update preview 500ms after user stops typing // Store current code state state.userCodeBeforeValidation = elements.codeInput.value; } // Update navigation buttons state function updateNavigationButtons() { elements.prevBtn.disabled = state.currentLessonIndex === 0; elements.nextBtn.disabled = !state.currentModule || state.currentLessonIndex === state.currentModule.lessons.length - 1; // Style changes for disabled buttons if (elements.prevBtn.disabled) { elements.prevBtn.classList.add("btn-disabled"); } else { elements.prevBtn.classList.remove("btn-disabled"); } if (elements.nextBtn.disabled) { elements.nextBtn.classList.add("btn-disabled"); } else { elements.nextBtn.classList.remove("btn-disabled"); } } // Go to the next lesson function nextLesson() { if (!state.currentModule) return; if (state.currentLessonIndex < state.currentModule.lessons.length - 1) { state.currentLessonIndex++; loadCurrentLesson(); } } // Go to the previous lesson function prevLesson() { if (state.currentLessonIndex > 0) { state.currentLessonIndex--; loadCurrentLesson(); } } // Run the user code function runCode() { const userCode = elements.codeInput.value; const lesson = state.currentModule.lessons[state.currentLessonIndex]; // Rotate the Run button icon const runButtonImg = document.querySelector("#run-btn img"); const runButtonRotationDegree = Number(runButtonImg.style.transform.match(/\d+/)?.pop() ?? 0); document.querySelector("#run-btn img").style.transform = `rotate(${runButtonRotationDegree + 180}deg)`; // Always apply the code to the preview, regardless of validation result lessonEngine.applyUserCode(userCode, true); const validationResult = validateUserCode(userCode, lesson); // Add validation indicators based on validCases count if available if (validationResult.validCases) { const casesCount = typeof validationResult.validCases === "number" ? validationResult.validCases : Array.isArray(validationResult.validCases) ? validationResult.validCases.length : 1; elements.validationIndicators.innerHTML = `${Math.round((validationResult.validCases / validationResult.totalCases) * 100)}%`; } if (validationResult.isValid) { // Mark lesson as completed const moduleProgress = state.userProgress[state.currentModule.id]; if (!moduleProgress.completed.includes(state.currentLessonIndex)) { moduleProgress.completed.push(state.currentLessonIndex); saveUserProgress(); updateModuleSelectorButtonProgress(); } // Show success feedback with visual indicators showFeedback(true, validationResult.message || "Great job! Your code works correctly."); // Add this block to update the Run button to Re-run elements.runBtn.innerHTML = 'Re-run'; elements.runBtn.classList.add("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); } // 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"); // Enable the next button if not already on the last lesson if (state.currentLessonIndex < state.currentModule.lessons.length - 1) { elements.nextBtn.disabled = false; elements.nextBtn.classList.remove("btn-disabled"); } } else { // Reset any success indicators resetSuccessIndicators(); // Show error feedback (with friendly message) showFeedback(false, validationResult.message || "Not quite there yet! Let's try again."); } } // Show the module selector modal function showModuleSelector() { elements.modalTitle.textContent = "Select a Module"; // Create module buttons const moduleButtons = state.modules.map((module) => { const button = document.createElement("button"); button.classList.add("btn", "module-button"); button.style.display = "block"; button.style.width = "100%"; button.style.marginBottom = "10px"; button.style.padding = "15px"; button.style.textAlign = "left"; // Add completion status const progress = state.userProgress[module.id]; const completedCount = progress ? progress.completed.length : 0; const totalLessons = module.lessons.length; const percentComplete = Math.round((completedCount / totalLessons) * 100); button.innerHTML = ` ${module.title}
${module.description}
${completedCount}/${totalLessons} lessons completed
`; button.addEventListener("click", () => { selectModule(module.id); closeModal(); }); return button; }); // Clear and update modal content elements.modalContent.innerHTML = ""; moduleButtons.forEach((button) => { elements.modalContent.appendChild(button); }); // Show the modal elements.modalContainer.classList.remove("hidden"); } // Show help modal function showHelp() { elements.modalTitle.textContent = "Help"; elements.modalContent.innerHTML = `

How to Use Code Crispies

Code Crispies is an interactive platform for learning CSS through practical exercises.

Getting Started

Select a module from the sidebar to start learning. Each module contains a series of lessons focused on specific CSS concepts.

Completing Lessons

For each lesson:

  1. Read the instructions and objective
  2. Write your CSS code in the editor
  3. Click "Run" to test your solution
  4. If correct, you can proceed to the next lesson

Controls

Tips

`; elements.modalContainer.classList.remove("hidden"); } // Reset user progress function resetProgress() { elements.modalTitle.textContent = "Reset Progress"; elements.modalContent.innerHTML = `

Are you sure you want to reset all your progress? This cannot be undone.

`; document.getElementById("cancel-reset").addEventListener("click", closeModal); document.getElementById("confirm-reset").addEventListener("click", () => { localStorage.removeItem("codeCrispies.Progress"); localStorage.removeItem("codeCrispies.lastModuleId"); state.userProgress = {}; closeModal(); // Reload the current module if (state.currentModule) { const currentModuleId = state.currentModule.id; selectModule(currentModuleId); } else if (state.modules.length > 0) { selectModule(state.modules[0].id); } // Update progress indicator updateModuleSelectorButtonProgress(); }); elements.modalContainer.classList.remove("hidden"); } // Close the modal function closeModal() { elements.modalContainer.classList.add("hidden"); } // Handle clicks in the code editor to focus the input function handleEditorClick() { elements.codeInput.focus(); // Add a temporary highlight class to show where the cursor is elements.editorContent.classList.add("editor-focused"); // Remove the highlight after a short delay setTimeout(() => { elements.editorContent.classList.remove("editor-focused"); }, 300); } // Handle tab key in the code editor function handleTabKey(e) { if (e.key === "Tab") { e.preventDefault(); const start = e.target.selectionStart; const end = e.target.selectionEnd; // Add two spaces at cursor position e.target.value = e.target.value.substring(0, start) + " " + e.target.value.substring(end); // Move cursor position after the inserted spaces e.target.selectionStart = e.target.selectionEnd = start + 2; } } // Initialize the application function init() { loadUserProgress(); loadUserSettings(); initializeModules().catch(console.error); initFeedbackToggle(); // Event listeners elements.prevBtn.addEventListener("click", prevLesson); elements.nextBtn.addEventListener("click", nextLesson); elements.runBtn.addEventListener("click", runCode); elements.modalClose.addEventListener("click", closeModal); elements.moduleSelectorBtn.addEventListener("click", showModuleSelector); elements.resetBtn.addEventListener("click", resetProgress); elements.helpBtn.addEventListener("click", showHelp); elements.codeInput.addEventListener("click", handleEditorClick); // Also make the editor container clickable to focus the text area elements.editorContent.addEventListener("click", (e) => { elements.codeInput.focus(); }); // Load user settings elements.disableFeedbackToggle.addEventListener("change", (e) => { state.userSettings.disableFeedbackErrors = !e.target.checked; saveUserSettings(); }); // Add tab key handler for the code input elements.codeInput.addEventListener("keydown", handleTabKey); // Handle keyboard shortcuts document.addEventListener("keydown", (e) => { // Ctrl+Enter to run code if (e.ctrlKey && e.key === "Enter") { runCode(); e.preventDefault(); } }); } // Start the application init();