WIP: enhance code editor layout and implement live preview functionality
This commit is contained in:
190
src/app.js
190
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 <span style="font-size: 0.8em; opacity: 0.8;">${percentComplete}%</span>`;
|
||||
@@ -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 = `
|
||||
<strong>${module.title}</strong>
|
||||
<div style="margin-top: 5px; font-size: 0.8rem; color: var(--light-text);">
|
||||
${module.description}
|
||||
</div>
|
||||
<div style="margin-top: 8px; height: 6px; background-color: #f0f0f0; border-radius: 3px;">
|
||||
<div style="height: 100%; width: ${percentComplete}%; background-color: var(--primary-color); border-radius: 3px;"></div>
|
||||
</div>
|
||||
<div style="margin-top: 5px; font-size: 0.8rem; text-align: right;">
|
||||
${completedCount}/${totalLessons} lessons completed
|
||||
</div>
|
||||
`;
|
||||
<strong>${module.title}</strong>
|
||||
<div style="margin-top: 5px; font-size: 0.8rem; color: var(--light-text);">
|
||||
${module.description}
|
||||
</div>
|
||||
<div style="margin-top: 8px; height: 6px; background-color: #f0f0f0; border-radius: 3px;">
|
||||
<div style="height: 100%; width: ${percentComplete}%; background-color: var(--primary-color); border-radius: 3px;"></div>
|
||||
</div>
|
||||
<div style="margin-top: 5px; font-size: 0.8rem; text-align: right;">
|
||||
${completedCount}/${totalLessons} lessons completed
|
||||
</div>
|
||||
`;
|
||||
|
||||
button.addEventListener("click", () => {
|
||||
selectModule(module.id);
|
||||
@@ -341,38 +406,39 @@ function showHelp() {
|
||||
elements.modalTitle.textContent = "Help";
|
||||
|
||||
elements.modalContent.innerHTML = `
|
||||
<h3>How to Use Code Crispies</h3>
|
||||
<p>Code Crispies is an interactive platform for learning CSS through practical exercises.</p>
|
||||
|
||||
<h4>Getting Started</h4>
|
||||
<p>Select a module from the sidebar to start learning. Each module contains a series of lessons focused on specific CSS concepts.</p>
|
||||
|
||||
<h4>Completing Lessons</h4>
|
||||
<p>For each lesson:</p>
|
||||
<ol>
|
||||
<li>Read the instructions and objective</li>
|
||||
<li>Write your CSS code in the editor</li>
|
||||
<li>Click "Run" to test your solution</li>
|
||||
<li>If correct, you can proceed to the next lesson</li>
|
||||
</ol>
|
||||
|
||||
<h4>Controls</h4>
|
||||
<ul>
|
||||
<li><strong>Run</strong> - Test your CSS code</li>
|
||||
<li><strong>Previous/Next</strong> - Navigate between lessons</li>
|
||||
<li><strong>Progress</strong> - Select a different learning module</li>
|
||||
<li><strong>Reset Progress</strong> - Clear all your saved progress</li>
|
||||
</ul>
|
||||
|
||||
<h4>Tips</h4>
|
||||
<ul>
|
||||
<li>Use the preview area to see how your CSS affects the elements</li>
|
||||
<li>Your progress is automatically saved in your browser storage</li>
|
||||
<li>You can revisit completed lessons at any time</li>
|
||||
<li>Press Tab in the code editor to indent with two spaces</li>
|
||||
<li>Use Ctrl+Enter to quickly run your code</li>
|
||||
</ul>
|
||||
`;
|
||||
<h3>How to Use Code Crispies</h3>
|
||||
<p>Code Crispies is an interactive platform for learning CSS through practical exercises.</p>
|
||||
|
||||
<h4>Getting Started</h4>
|
||||
<p>Select a module from the sidebar to start learning. Each module contains a series of lessons focused on specific CSS concepts.</p>
|
||||
|
||||
<h4>Completing Lessons</h4>
|
||||
<p>For each lesson:</p>
|
||||
<ol>
|
||||
<li>Read the instructions and objective</li>
|
||||
<li>Write your CSS code in the editor</li>
|
||||
<li>Click "Run" to test your solution</li>
|
||||
<li>If correct, you can proceed to the next lesson</li>
|
||||
</ol>
|
||||
|
||||
<h4>Controls</h4>
|
||||
<ul>
|
||||
<li><strong>Run</strong> - Test your CSS code and apply it to the preview</li>
|
||||
<li><strong>Previous/Next</strong> - Navigate between lessons</li>
|
||||
<li><strong>Progress</strong> - Select a different learning module</li>
|
||||
<li><strong>Reset Progress</strong> - Clear all your saved progress</li>
|
||||
</ul>
|
||||
|
||||
<h4>Tips</h4>
|
||||
<ul>
|
||||
<li>Your code changes will automatically preview as you type</li>
|
||||
<li>The preview area shows how your CSS affects the elements</li>
|
||||
<li>Your progress is automatically saved in your browser storage</li>
|
||||
<li>You can revisit completed lessons at any time</li>
|
||||
<li>Press Tab in the code editor to indent with two spaces</li>
|
||||
<li>Use Ctrl+Enter to quickly run your code</li>
|
||||
</ul>
|
||||
`;
|
||||
|
||||
elements.modalContainer.classList.remove("hidden");
|
||||
}
|
||||
@@ -382,12 +448,12 @@ function resetProgress() {
|
||||
elements.modalTitle.textContent = "Reset Progress";
|
||||
|
||||
elements.modalContent.innerHTML = `
|
||||
<p>Are you sure you want to reset all your progress? This cannot be undone.</p>
|
||||
<div style="display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px;">
|
||||
<button id="cancel-reset" class="btn">Cancel</button>
|
||||
<button id="confirm-reset" class="btn btn-primary">Reset Progress</button>
|
||||
</div>
|
||||
`;
|
||||
<p>Are you sure you want to reset all your progress? This cannot be undone.</p>
|
||||
<div style="display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px;">
|
||||
<button id="cancel-reset" class="btn">Cancel</button>
|
||||
<button id="confirm-reset" class="btn btn-primary">Reset Progress</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById("cancel-reset").addEventListener("click", closeModal);
|
||||
document.getElementById("confirm-reset").addEventListener("click", () => {
|
||||
@@ -479,4 +545,4 @@ function init() {
|
||||
}
|
||||
|
||||
// Start the application
|
||||
init();
|
||||
init();
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>${combinedCSS}</style>
|
||||
</head>
|
||||
<body>
|
||||
${previewHTML || "<div>No preview available</div>"}
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>${combinedCSS}</style>
|
||||
</head>
|
||||
<body>
|
||||
${previewHTML || "<div>No preview available</div>"}
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/main.css
17
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;
|
||||
|
||||
Reference in New Issue
Block a user