diff --git a/src/app.js b/src/app.js index 42d5bf7..54c2877 100644 --- a/src/app.js +++ b/src/app.js @@ -8,7 +8,8 @@ const state = { currentModule: null, currentLessonIndex: 0, modules: [], - userProgress: {} // Format: { moduleId: { completed: [0, 2, 3], current: 4 } } + userProgress: {}, // Format: { moduleId: { completed: [0, 2, 3], current: 4 } } + userCodeBeforeValidation: "" // Track user code state before validation }; // DOM elements @@ -97,14 +98,14 @@ function updateModuleSelectorButtonProgress() { const progressBar = document.createElement("div"); progressBar.className = "progress-indicator"; progressBar.style.cssText = ` - position: absolute; - bottom: 0; - left: 0; - height: 3px; - width: ${percentComplete}%; - background-color: var(--success-color); - border-radius: 0 3px 3px 0; - `; + position: absolute; + bottom: 0; + left: 0; + height: 3px; + width: ${percentComplete}%; + background-color: var(--success-color); + border-radius: 0 3px 3px 0; + `; // Add progress percentage text elements.moduleSelectorBtn.innerHTML = `Progress ${percentComplete}%`; @@ -158,6 +159,34 @@ function resetSuccessIndicators() { elements.taskInstruction.classList.remove("success-instruction"); } +// Configure editor layout based on display type +function configureEditorLayout(lesson) { + // Default to block display if not specified + const displayType = lesson.editorDisplayType || "block"; + + // Reset classes + elements.codeEditor.classList.remove("inline-editor", "block-editor"); + elements.editorContent.classList.remove("inline-mode", "block-mode"); + + // Apply appropriate layout class + if (displayType === "inline") { + elements.codeEditor.classList.add("inline-editor"); + elements.editorContent.classList.add("inline-mode"); + + // Add special styling for inline mode + elements.codeInput.style.display = "inline-block"; + elements.codeInput.style.width = lesson.inlineInputWidth || "auto"; + } else { + // Default block mode + elements.codeEditor.classList.add("block-editor"); + elements.editorContent.classList.add("block-mode"); + + // Reset styles for block mode + elements.codeInput.style.display = "block"; + elements.codeInput.style.width = "100%"; + } +} + // Load the current lesson function loadCurrentLesson() { if (!state.currentModule || !state.currentModule.lessons) { @@ -189,6 +218,9 @@ function loadCurrentLesson() { lesson ); + // Configure editor layout based on lesson settings + configureEditorLayout(lesson); + // Update level indicator renderLevelIndicator(elements.levelIndicator, state.currentLessonIndex + 1, state.currentModule.lessons.length); @@ -204,6 +236,39 @@ function loadCurrentLesson() { // 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); + }, 500); // Update preview 500ms after user stops typing + + // Store current code state + state.userCodeBeforeValidation = elements.codeInput.value; } // Update navigation buttons state @@ -248,6 +313,9 @@ function runCode() { const userCode = elements.codeInput.value; const lesson = state.currentModule.lessons[state.currentLessonIndex]; + // Always apply the code to the preview, regardless of validation result + lessonEngine.applyUserCode(userCode, true); + const validationResult = validateUserCode(userCode, lesson); if (validationResult.isValid) { @@ -268,9 +336,6 @@ function runCode() { elements.nextBtn.classList.add("success"); elements.taskInstruction.classList.add("success-instruction"); - // Apply the code to see the result - lessonEngine.applyUserCode(userCode); - // Enable the next button if not already on the last lesson if (state.currentLessonIndex < state.currentModule.lessons.length - 1) { elements.nextBtn.disabled = false; @@ -306,17 +371,17 @@ function showModuleSelector() { const percentComplete = Math.round((completedCount / totalLessons) * 100); button.innerHTML = ` - ${module.title} -
- ${module.description} -
-
-
-
-
- ${completedCount}/${totalLessons} lessons completed -
- `; + ${module.title} +
+ ${module.description} +
+
+
+
+
+ ${completedCount}/${totalLessons} lessons completed +
+ `; button.addEventListener("click", () => { selectModule(module.id); @@ -341,38 +406,39 @@ 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. -
  3. Write your CSS code in the editor
  4. -
  5. Click "Run" to test your solution
  6. -
  7. If correct, you can proceed to the next lesson
  8. -
- -

Controls

- - -

Tips

- - `; +

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. +
  3. Write your CSS code in the editor
  4. +
  5. Click "Run" to test your solution
  6. +
  7. If correct, you can proceed to the next lesson
  8. +
+ +

Controls

+ + +

Tips

+ + `; elements.modalContainer.classList.remove("hidden"); } @@ -382,12 +448,12 @@ function resetProgress() { elements.modalTitle.textContent = "Reset Progress"; elements.modalContent.innerHTML = ` -

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

-
- - -
- `; +

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", () => { @@ -479,4 +545,4 @@ function init() { } // Start the application -init(); +init(); \ No newline at end of file diff --git a/src/config/lessons.js b/src/config/lessons.js index 0bde848..03578b1 100644 --- a/src/config/lessons.js +++ b/src/config/lessons.js @@ -16,16 +16,16 @@ import responsiveConfig from "../../lessons/08-responsive.json"; // Module store const moduleStore = [ - basicSelectorsConfig - // basicsConfig, - // boxModelConfig, - // selectorsConfig, - // colorsConfig, - // typographyConfig, - // unitVariablesConfig, - // transitionsAnimationsConfig, - // layoutConfig, - // responsiveConfig + basicSelectorsConfig, + basicsConfig, + boxModelConfig, + selectorsConfig, + colorsConfig, + typographyConfig, + unitVariablesConfig, + transitionsAnimationsConfig, + layoutConfig, + responsiveConfig ]; /** @@ -83,6 +83,15 @@ function validateModuleConfig(config) { config.lessons.forEach((lesson, index) => { if (!lesson.title) throw new Error(`Lesson ${index} missing "title"`); if (!lesson.previewHTML) throw new Error(`Lesson ${index} missing "previewHTML"`); + + // Apply defaults for new properties if they don't exist + if (lesson.editorDisplayType === undefined) { + lesson.editorDisplayType = "block"; // Default to block display + } + + if (lesson.editorDisplayType === "inline" && lesson.inlineInputWidth === undefined) { + lesson.inlineInputWidth = "auto"; // Default width for inline input + } }); } @@ -111,3 +120,36 @@ export function addCustomModule(moduleConfig) { return false; } } + +/** + * Convert a module to include the enhanced schema with editorDisplayType + * @param {Object} moduleConfig - The module configuration to convert + * @returns {Object} The enhanced module configuration + */ +export function enhanceModuleSchema(moduleConfig) { + if (!moduleConfig || !moduleConfig.lessons) return moduleConfig; + + const enhancedModule = {...moduleConfig}; + + enhancedModule.lessons = moduleConfig.lessons.map(lesson => { + const enhancedLesson = {...lesson}; + + // Apply defaults for new properties if they don't exist + if (enhancedLesson.editorDisplayType === undefined) { + enhancedLesson.editorDisplayType = "block"; // Default to block display + } + + if (enhancedLesson.editorDisplayType === "inline" && enhancedLesson.inlineInputWidth === undefined) { + enhancedLesson.inlineInputWidth = "auto"; // Default width for inline input + } + + return enhancedLesson; + }); + + return enhancedModule; +} + +// Enhance all modules on load to ensure they have the new schema properties +moduleStore.forEach((module, index) => { + moduleStore[index] = enhanceModuleSchema(module); +}); \ No newline at end of file diff --git a/src/impl/LessonEngine.js b/src/impl/LessonEngine.js index cd9bb1f..ad39063 100644 --- a/src/impl/LessonEngine.js +++ b/src/impl/LessonEngine.js @@ -11,6 +11,7 @@ export class LessonEngine { this.userCode = ""; this.currentModule = null; this.currentLessonIndex = 0; + this.lastRenderedCode = ""; // Track last applied code to prevent unnecessary re-renders } /** @@ -32,6 +33,7 @@ export class LessonEngine { setLesson(lesson) { this.currentLesson = lesson; this.userCode = lesson.initialCode || ""; + this.lastRenderedCode = ""; // Reset last rendered code this.renderPreview(); } @@ -73,12 +75,34 @@ export class LessonEngine { /** * Apply user-written CSS to the preview area * @param {string} code - User CSS code + * @param {boolean} forceUpdate - Force update the preview even if code hasn't changed */ - applyUserCode(code) { + applyUserCode(code, forceUpdate = false) { if (!this.currentLesson) return; this.userCode = code; - this.renderPreview(); + + // Only re-render if code changed or forced update + if (forceUpdate || this.lastRenderedCode !== code) { + this.lastRenderedCode = code; + this.renderPreview(); + } + } + + /** + * Get the complete CSS by combining all parts + * @returns {string} The complete CSS + */ + getCompleteCss() { + if (!this.currentLesson) return ""; + + const { codePrefix, codeSuffix } = this.currentLesson; + + return ` + ${codePrefix || ""} + ${this.userCode || ""} + ${codeSuffix || ""} + `; } /** @@ -103,32 +127,35 @@ export class LessonEngine { container.innerHTML = ""; container.appendChild(iframe); + // Get the complete CSS by combining all parts + const userCssWithWrapper = this.getCompleteCss(); + // Create the complete CSS by combining base CSS with user code and sandbox CSS const combinedCSS = ` - /* Base CSS */ - ${previewBaseCSS || ""} - - /* User Code */ - ${this.userCode || ""} - - /* Sandbox CSS (for visualizing the exercise) */ - ${sandboxCSS || ""} - `; + /* Base CSS */ + ${previewBaseCSS || ""} + + /* User Code */ + ${userCssWithWrapper || ""} + + /* Sandbox CSS (for visualizing the exercise) */ + ${sandboxCSS || ""} + `; // Write the content to the iframe const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; iframeDoc.open(); iframeDoc.write(` - - - - - - - ${previewHTML || "
No preview available
"} - - - `); + + + + + + + ${previewHTML || "
No preview available
"} + + + `); iframeDoc.close(); } @@ -176,7 +203,7 @@ export class LessonEngine { timestamp: new Date().toISOString() }; - localStorage.setItem("cssQuest_progress", JSON.stringify(progressData)); + localStorage.setItem("codeCrispies.progress", JSON.stringify(progressData)); } /** @@ -185,7 +212,7 @@ export class LessonEngine { * @returns {Object|null} Loaded progress data or null if not found */ loadProgress(modules) { - const savedProgress = localStorage.getItem("cssQuest_progress"); + const savedProgress = localStorage.getItem("codeCrispies.progress"); if (!savedProgress) return null; try { @@ -225,6 +252,6 @@ export class LessonEngine { * Clear all saved progress */ clearProgress() { - localStorage.removeItem("cssQuest_progress"); + localStorage.removeItem("codeCrispies.progress"); } -} +} \ No newline at end of file diff --git a/src/main.css b/src/main.css index 4ea214c..9d5fa67 100644 --- a/src/main.css +++ b/src/main.css @@ -271,6 +271,15 @@ code { overflow: hidden; } +.code-editor.block-editor { + display: block; +} + +.code-editor.inline-editor { + display: inline-block; + width: 100%; +} + .editor-header { background-color: var(--code-bg); padding: var(--spacing-xs) var(--spacing-md); @@ -283,11 +292,13 @@ code { } .editor-content { + display: flex; + flex-direction: column; background-color: var(--editor-bg); color: #d4d4d4; padding: var(--spacing-md); - margin-bottom: 4rem; - overflow-y: auto; + /*margin-bottom: 4rem;*/ + overflow-y: scroll; height: 100%; font-family: var(--font-code); font-size: 14px; @@ -314,6 +325,8 @@ code { } .code-input { + flex: 1; + display: block; background-color: transparent; color: #d4d4d4; border: none;