refactor: redesign layout to Flexbox Froggy style with slide-out sidebar

- Implement 50/50 split layout (left: instructions + editor, right: preview)
- Replace always-visible sidebar with slide-out drawer menu
- Move footer, progress, and settings into sidebar
- Add toggleable expected result overlay (hidden by default)
- Create new hint system with step progress indicators
- Add ghost button styles for modal and text button for sidebar reset
- Fix HTML lesson task instruction and typo ("important" not "importing")
- Add padding to preview frames to prevent corner clipping
- Optimize layout for iPadOS and tablet devices
This commit is contained in:
2025-12-21 23:20:07 +01:00
parent 862d29aa19
commit db4f143924
4 changed files with 965 additions and 1216 deletions

View File

@@ -10,12 +10,12 @@
"id": "block-vs-inline-intro", "id": "block-vs-inline-intro",
"title": "Block vs Inline Elements", "title": "Block vs Inline Elements",
"description": "HTML elements fall into two main categories:<br><br><strong>Block elements</strong> (containers) start on a new line and take full width. Examples: <kbd>&lt;div&gt;</kbd>, <kbd>&lt;p&gt;</kbd>, <kbd>&lt;h1&gt;</kbd>, <kbd>&lt;section&gt;</kbd><br><br><strong>Inline elements</strong> flow within text and only take needed width. Examples: <kbd>&lt;span&gt;</kbd>, <kbd>&lt;a&gt;</kbd>, <kbd>&lt;strong&gt;</kbd>, <kbd>&lt;em&gt;</kbd>", "description": "HTML elements fall into two main categories:<br><br><strong>Block elements</strong> (containers) start on a new line and take full width. Examples: <kbd>&lt;div&gt;</kbd>, <kbd>&lt;p&gt;</kbd>, <kbd>&lt;h1&gt;</kbd>, <kbd>&lt;section&gt;</kbd><br><br><strong>Inline elements</strong> flow within text and only take needed width. Examples: <kbd>&lt;span&gt;</kbd>, <kbd>&lt;a&gt;</kbd>, <kbd>&lt;strong&gt;</kbd>, <kbd>&lt;em&gt;</kbd>",
"task": "Create a paragraph with a <kbd>&lt;strong&gt;</kbd> word inside it. Notice how the paragraph is a block element (takes full width) while strong is inline (flows with text).", "task": "Wrap the word <kbd>important</kbd> with <kbd>&lt;strong&gt;</kbd> tags to make it bold. Notice how the paragraph (block) takes full width while strong (inline) flows with text.",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 20px; } p { background: #e3f2fd; padding: 10px; } strong { background: #ffecb3; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 20px; } p { background: #e3f2fd; padding: 10px; } strong { background: #ffecb3; }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "<p>This is a paragraph with an important word.</p>", "initialCode": "<p>This is a paragraph with an important word.</p>",
"solutionCode": "<p>This is a paragraph with a <strong>important</strong> word.</p>", "solutionCode": "<p>This is a paragraph with an <strong>important</strong> word.</p>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
@@ -26,7 +26,7 @@
{ {
"type": "parent_child", "type": "parent_child",
"value": { "parent": "p", "child": "strong" }, "value": { "parent": "p", "child": "strong" },
"message": "Place a <strong> element inside your paragraph" "message": "Wrap the word 'important' with <strong> tags"
} }
] ]
}, },

View File

@@ -1,66 +1,130 @@
import { LessonEngine } from "./impl/LessonEngine.js"; import { LessonEngine } from "./impl/LessonEngine.js";
import { renderLesson, renderModuleList, renderLevelIndicator, showFeedback, updateActiveLessonInSidebar } from "./helpers/renderer.js"; import { renderLesson, renderModuleList, renderLevelIndicator, updateActiveLessonInSidebar } from "./helpers/renderer.js";
import { loadModules } from "./config/lessons.js"; import { loadModules } from "./config/lessons.js";
// Simplified state - LessonEngine now manages lesson state and progress // Simplified state - LessonEngine now manages lesson state and progress
const state = { const state = {
userSettings: { userSettings: {
disableFeedbackErrors: false disableFeedbackErrors: false
} },
showExpected: false
}; };
// DOM elements // DOM elements - updated for new layout
const elements = { const elements = {
moduleList: document.querySelector(".module-list"), // Header
menuBtn: document.getElementById("menu-btn"),
helpBtn: document.getElementById("help-btn"),
// Left panel
lessonTitle: document.getElementById("lesson-title"), lessonTitle: document.getElementById("lesson-title"),
lessonDescription: document.getElementById("lesson-description"), lessonDescription: document.getElementById("lesson-description"),
taskInstruction: document.getElementById("task-instruction"), taskInstruction: document.getElementById("task-instruction"),
previewArea: document.getElementById("preview-area"),
editorPrefix: document.getElementById("editor-prefix"),
codeInput: document.getElementById("code-input"), codeInput: document.getElementById("code-input"),
editorSuffix: document.getElementById("editor-suffix"), runBtn: document.getElementById("run-btn"),
hintArea: document.getElementById("hint-area"),
validationIndicators: document.querySelector(".validation-indicators-container"),
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"), prevBtn: document.getElementById("prev-btn"),
nextBtn: document.getElementById("next-btn"), nextBtn: document.getElementById("next-btn"),
runBtn: document.getElementById("run-btn"),
levelIndicator: document.getElementById("level-indicator"), 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"),
// Modal
modalContainer: document.getElementById("modal-container"), modalContainer: document.getElementById("modal-container"),
modalTitle: document.getElementById("modal-title"), modalTitle: document.getElementById("modal-title"),
modalContent: document.getElementById("modal-content"), modalContent: document.getElementById("modal-content"),
modalClose: document.getElementById("modal-close"), 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 - now the single source of truth // Initialize the lesson engine - now the single source of truth
const lessonEngine = new LessonEngine(); const lessonEngine = new LessonEngine();
// Load user progress from localStorage // ================= SIDEBAR FUNCTIONS =================
function loadUserProgress() {
const savedProgress = localStorage.getItem("codeCrispies.progress"); function openSidebar() {
if (savedProgress) { elements.sidebarDrawer.classList.add("open");
state.userProgress = JSON.parse(savedProgress); elements.sidebarBackdrop.classList.add("visible");
}
function closeSidebar() {
elements.sidebarDrawer.classList.remove("open");
elements.sidebarBackdrop.classList.remove("visible");
}
// ================= 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");
} }
} }
// Save user progress to localStorage // ================= HINT SYSTEM =================
function saveUserProgress() {
localStorage.setItem("codeCrispies.progress", JSON.stringify(state.userProgress)); function showHint(message, step, total, isSuccess = false) {
const hintClass = isSuccess ? "hint hint-success" : "hint";
elements.hintArea.innerHTML = `
<div class="${hintClass}">
<span class="hint-progress">${step}/${total}</span>
<span class="hint-message">${message}</span>
</div>
`;
} }
function clearHint() {
elements.hintArea.innerHTML = "";
}
function showSuccessHint(message) {
elements.hintArea.innerHTML = `
<div class="hint hint-success">
<span class="hint-progress">✓</span>
<span class="hint-message">${message}</span>
</div>
`;
}
// ================= 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() { function loadUserSettings() {
const savedSettings = localStorage.getItem("codeCrispies.settings"); const savedSettings = localStorage.getItem("codeCrispies.settings");
if (savedSettings) { if (savedSettings) {
try { try {
const settings = JSON.parse(savedSettings); const settings = JSON.parse(savedSettings);
state.userSettings = { ...state.userSettings, ...settings }; state.userSettings = { ...state.userSettings, ...settings };
// Apply saved settings to UI
elements.disableFeedbackToggle.checked = !state.userSettings.disableFeedbackErrors; elements.disableFeedbackToggle.checked = !state.userSettings.disableFeedbackErrors;
} catch (e) { } catch (e) {
console.error("Error loading user settings:", e); console.error("Error loading user settings:", e);
@@ -72,14 +136,8 @@ function saveUserSettings() {
localStorage.setItem("codeCrispies.settings", JSON.stringify(state.userSettings)); localStorage.setItem("codeCrispies.settings", JSON.stringify(state.userSettings));
} }
function initFeedbackToggle() { // ================= MODULE INITIALIZATION =================
elements.disableFeedbackToggle.addEventListener("change", (e) => {
state.userSettings.disableFeedbackErrors = !e.target.checked;
saveUserSettings();
});
}
// Initialize the module list
async function initializeModules() { async function initializeModules() {
try { try {
const modules = await loadModules(); const modules = await loadModules();
@@ -98,45 +156,15 @@ async function initializeModules() {
selectModule(modules[0].id); selectModule(modules[0].id);
} }
// Update progress indicator on module selector button updateProgressDisplay();
updateModuleSelectorButtonProgress();
} catch (error) { } catch (error) {
console.error("Failed to load modules:", error); console.error("Failed to load modules:", error);
elements.lessonDescription.textContent = "Failed to load modules. Please refresh the page."; elements.lessonDescription.textContent = "Failed to load modules. Please refresh the page.";
} }
} }
// Update progress indicator on module selector button // ================= MODULE/LESSON SELECTION =================
function updateModuleSelectorButtonProgress() {
const stats = lessonEngine.getProgressStats();
// Create progress indicator
const progressBar = document.createElement("div");
progressBar.className = "progress-indicator";
progressBar.style.cssText = `
position: absolute;
bottom: 0;
left: 0;
height: 3px;
width: ${stats.percentComplete}%;
background-color: var(--primary-light);
border-radius: 0 3px 3px 0;
`;
// Add progress percentage text
elements.moduleSelectorBtn.innerHTML = `Progress <span style="font-size: 0.8em; opacity: 0.8;">${stats.percentComplete}%</span>`;
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 - delegate to LessonEngine
function selectModule(moduleId) { function selectModule(moduleId) {
const success = lessonEngine.setModuleById(moduleId); const success = lessonEngine.setModuleById(moduleId);
if (!success) return; if (!success) return;
@@ -151,30 +179,38 @@ function selectModule(moduleId) {
}); });
loadCurrentLesson(); loadCurrentLesson();
// Reset any success indicators
resetSuccessIndicators(); resetSuccessIndicators();
// Close sidebar after selection on mobile
if (window.innerWidth <= 768) {
closeSidebar();
}
} }
function selectLesson(moduleId, lessonIndex) { function selectLesson(moduleId, lessonIndex) {
// Select the module first if it's not already selected
const currentState = lessonEngine.getCurrentState(); const currentState = lessonEngine.getCurrentState();
if (!currentState.module || currentState.module.id !== moduleId) { if (!currentState.module || currentState.module.id !== moduleId) {
lessonEngine.setModuleById(moduleId); lessonEngine.setModuleById(moduleId);
} }
// Set the lesson
lessonEngine.setLessonByIndex(lessonIndex); lessonEngine.setLessonByIndex(lessonIndex);
loadCurrentLesson(); loadCurrentLesson();
// Close sidebar after selection on mobile
if (window.innerWidth <= 768) {
closeSidebar();
}
} }
// Reset success indicators // ================= LESSON LOADING =================
function resetSuccessIndicators() { function resetSuccessIndicators() {
elements.codeEditor.classList.remove("success-highlight"); elements.codeEditor.classList.remove("success-highlight");
elements.lessonTitle.classList.remove("success-text"); elements.lessonTitle.classList.remove("success-text");
elements.nextBtn.classList.remove("success"); elements.nextBtn.classList.remove("success");
elements.taskInstruction.classList.remove("success-instruction"); elements.taskInstruction.classList.remove("success-instruction");
elements.runBtn.classList.remove("re-run"); elements.runBtn.classList.remove("success");
elements.previewWrapper?.classList.remove("matched");
} }
function updateEditorForMode(mode) { function updateEditorForMode(mode) {
@@ -201,12 +237,6 @@ function updateEditorForMode(mode) {
if (editorLabel) editorLabel.textContent = config.label; if (editorLabel) editorLabel.textContent = config.label;
} }
// Configure editor layout based on display type
function resetEditorLayout(lesson) {
elements.validationIndicators.innerHTML = "";
}
// Load the current lesson - now delegates to LessonEngine
function loadCurrentLesson() { function loadCurrentLesson() {
const engineState = lessonEngine.getCurrentState(); const engineState = lessonEngine.getCurrentState();
@@ -223,29 +253,38 @@ function loadCurrentLesson() {
// Reset any success indicators // Reset any success indicators
resetSuccessIndicators(); 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 // Update UI
renderLesson( renderLesson(
elements.lessonTitle, elements.lessonTitle,
elements.lessonDescription, elements.lessonDescription,
elements.taskInstruction, elements.taskInstruction,
elements.previewArea, elements.previewArea,
elements.editorPrefix, null, // editorPrefix no longer used
elements.codeInput, elements.codeInput,
elements.editorSuffix, null, // editorSuffix no longer used
lesson lesson
); );
// Set user code in input // Set user code in input
elements.codeInput.value = engineState.userCode; elements.codeInput.value = engineState.userCode;
// Configure editor layout based on lesson settings // Reset validation indicators
resetEditorLayout(lesson); elements.validationIndicators.innerHTML = "";
// Update Run button text based on completion status // Update Run button text based on completion status
if (engineState.isCompleted) { if (engineState.isCompleted) {
elements.runBtn.innerHTML = '<img src="./gear.svg" />Re-run'; elements.runBtn.innerHTML = '<img src="./gear.svg" alt="" />Re-run';
// Add completion badge next to title if not already present // Add completion badge if not present
if (!document.querySelector(".completion-badge")) { if (!document.querySelector(".completion-badge")) {
const badge = document.createElement("span"); const badge = document.createElement("span");
badge.className = "completion-badge"; badge.className = "completion-badge";
@@ -253,13 +292,11 @@ function loadCurrentLesson() {
elements.lessonTitle.appendChild(badge); elements.lessonTitle.appendChild(badge);
} }
} else { } else {
elements.runBtn.innerHTML = '<img src="./gear.svg" />Run'; elements.runBtn.innerHTML = '<img src="./gear.svg" alt="" />Run';
// Remove completion badge if exists // Remove completion badge if exists
const badge = document.querySelector(".completion-badge"); const badge = document.querySelector(".completion-badge");
if (badge) { if (badge) badge.remove();
badge.remove();
}
} }
// Update level indicator // Update level indicator
@@ -271,64 +308,50 @@ function loadCurrentLesson() {
// Update navigation buttons // Update navigation buttons
updateNavigationButtons(); updateNavigationButtons();
// Update progress indicator on module selector button // Update progress display
updateModuleSelectorButtonProgress(); updateProgressDisplay();
// Focus on the code editor by default // Focus on the code editor
elements.codeInput.focus(); elements.codeInput.focus();
// Render the expected/solution preview for comparison // Render the expected/solution preview
lessonEngine.renderExpectedPreview(); lessonEngine.renderExpectedPreview();
// Track live changes and update preview when the user pauses typing // Setup live preview
setupLivePreview(); setupLivePreview();
} }
// Setup live preview functionality // ================= LIVE PREVIEW =================
let previewTimer = null;
function setupLivePreview() {
// Clear previous event listener if any
elements.codeInput.removeEventListener("input", handleUserInput);
// Add new event listener let previewTimer = null;
function setupLivePreview() {
elements.codeInput.removeEventListener("input", handleUserInput);
elements.codeInput.addEventListener("input", handleUserInput); elements.codeInput.addEventListener("input", handleUserInput);
} }
// Handle user input with debounced preview updates
function handleUserInput() { function handleUserInput() {
// Clear the previous timer
if (previewTimer) { if (previewTimer) {
clearTimeout(previewTimer); clearTimeout(previewTimer);
} }
// Set a new timer for preview update after user stops typing
previewTimer = setTimeout(() => { previewTimer = setTimeout(() => {
runCode(); runCode();
}, 800); // Update preview 800ms after user stops typing }, 800);
} }
// Update navigation buttons state // ================= NAVIGATION =================
function updateNavigationButtons() { function updateNavigationButtons() {
const engineState = lessonEngine.getCurrentState(); const engineState = lessonEngine.getCurrentState();
elements.prevBtn.disabled = !engineState.canGoPrev; elements.prevBtn.disabled = !engineState.canGoPrev;
elements.nextBtn.disabled = !engineState.canGoNext; elements.nextBtn.disabled = !engineState.canGoNext;
// Style changes for disabled buttons elements.prevBtn.classList.toggle("btn-disabled", !engineState.canGoPrev);
if (elements.prevBtn.disabled) { elements.nextBtn.classList.toggle("btn-disabled", !engineState.canGoNext);
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 - delegate to LessonEngine
function nextLesson() { function nextLesson() {
const success = lessonEngine.nextLesson(); const success = lessonEngine.nextLesson();
if (success) { if (success) {
@@ -336,7 +359,6 @@ function nextLesson() {
} }
} }
// Go to the previous lesson - delegate to LessonEngine
function prevLesson() { function prevLesson() {
const success = lessonEngine.previousLesson(); const success = lessonEngine.previousLesson();
if (success) { if (success) {
@@ -344,14 +366,17 @@ function prevLesson() {
} }
} }
// Run the user code - now uses LessonEngine validation // ================= CODE EXECUTION =================
function runCode() { function runCode() {
const userCode = elements.codeInput.value; const userCode = elements.codeInput.value;
// Rotate the Run button icon // Rotate the Run button icon
const runButtonImg = document.querySelector("#run-btn img"); const runButtonImg = document.querySelector("#run-btn img");
const runButtonRotationDegree = Number(runButtonImg.style.transform.match(/\d+/)?.pop() ?? 0); if (runButtonImg) {
document.querySelector("#run-btn img").style.transform = `rotate(${runButtonRotationDegree + 180}deg)`; 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 // Apply the code to the preview via LessonEngine
lessonEngine.applyUserCode(userCode, true); lessonEngine.applyUserCode(userCode, true);
@@ -359,27 +384,21 @@ function runCode() {
// Validate code using LessonEngine // Validate code using LessonEngine
const validationResult = lessonEngine.validateCode(); const validationResult = lessonEngine.validateCode();
// Add validation indicators based on validCases count if available // Update validation indicators
if (validationResult.validCases) { if (validationResult.totalCases > 0) {
const casesCount = const percent = Math.round((validationResult.validCases / validationResult.totalCases) * 100);
typeof validationResult.validCases === "number" elements.validationIndicators.innerHTML = `${percent}%`;
? validationResult.validCases
: Array.isArray(validationResult.validCases)
? validationResult.validCases.length
: 1;
elements.validationIndicators.innerHTML = `${Math.round((validationResult.validCases / validationResult.totalCases) * 100)}%`;
} }
if (validationResult.isValid) { if (validationResult.isValid) {
// Show success feedback with visual indicators // Show success hint
showFeedback(true, validationResult.message || "Great job! Your code works correctly."); showSuccessHint(validationResult.message || "Great job! Your code works correctly.");
// Update the Run button to Re-run // Update Run button
elements.runBtn.innerHTML = '<img src="./gear.svg" />Re-run'; elements.runBtn.innerHTML = '<img src="./gear.svg" alt="" />Re-run';
elements.runBtn.classList.add("re-run"); elements.runBtn.classList.add("success");
// Add completion badge if not present // Add completion badge
if (!document.querySelector(".completion-badge")) { if (!document.querySelector(".completion-badge")) {
const badge = document.createElement("span"); const badge = document.createElement("span");
badge.className = "completion-badge"; badge.className = "completion-badge";
@@ -393,174 +412,97 @@ function runCode() {
elements.nextBtn.classList.add("success"); elements.nextBtn.classList.add("success");
elements.taskInstruction.classList.add("success-instruction"); elements.taskInstruction.classList.add("success-instruction");
// Show merge animation for side-by-side comparison // Show match animation
lessonEngine.showMatchAnimation(); elements.previewWrapper?.classList.add("matched");
setTimeout(() => {
elements.previewWrapper?.classList.remove("matched");
}, 2500);
// Update navigation buttons
updateNavigationButtons(); updateNavigationButtons();
updateProgressDisplay();
// Update progress indicator
updateModuleSelectorButtonProgress();
} else { } else {
// Reset any success indicators // Reset success indicators
resetSuccessIndicators(); resetSuccessIndicators();
// Hide merge animation if it was showing // Show hint with step progress
lessonEngine.hideMatchAnimation(); const step = validationResult.validCases + 1;
const total = validationResult.totalCases;
// Show error feedback (with friendly message) // Only show hints if enabled
showFeedback(false, validationResult.message || "Not quite there yet! Let's try again."); if (!state.userSettings.disableFeedbackErrors) {
showHint(validationResult.message || "Keep trying!", step, total);
}
} }
} }
// Show the module selector modal // ================= MODALS =================
function showModuleSelector() {
elements.modalTitle.textContent = "Select a Module";
const engineState = lessonEngine.getCurrentState();
const modules = lessonEngine.modules;
// Create module buttons
const moduleButtons = 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 using LessonEngine
const completedCount = lessonEngine.userProgress[module.id]?.completed.length || 0;
const totalLessons = module.lessons.length;
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>
`;
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() { function showHelp() {
elements.modalTitle.textContent = "Help"; elements.modalTitle.textContent = "Help";
elements.modalContent.innerHTML = ` elements.modalContent.innerHTML = `
<h3>How to Use Code Crispies</h3> <h3>How to Use Code Crispies</h3>
<p>Code Crispies is an interactive platform for learning CSS through practical exercises.</p> <p>Code Crispies is an interactive platform for learning HTML, CSS, and Tailwind through practical exercises.</p>
<h4>Getting Started</h4> <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> <p>Open the menu (☰) to select a lesson module. Each module contains a series of lessons.</p>
<h4>Completing Lessons</h4> <h4>Completing Lessons</h4>
<p>For each lesson:</p>
<ol> <ol>
<li>Read the instructions and objective</li> <li>Read the instructions on the left</li>
<li>Write your CSS code in the editor</li> <li>Write your code in the editor</li>
<li>Click "Run" to test your solution</li> <li>Click "Run" or press Ctrl+Enter to test</li>
<li>If correct, you can proceed to the next lesson</li> <li>Follow the hints to fix any issues</li>
<li>Click "Next" when you're done</li>
</ol> </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> <h4>Tips</h4>
<ul> <ul>
<li>Your code changes will automatically preview as you type</li> <li>Click "Show Expected" to see the target result</li>
<li>The preview area shows how your CSS affects the elements</li> <li>Your progress is saved automatically</li>
<li>Your progress is automatically saved in your browser storage</li> <li>Use Tab for indentation</li>
<li>You can revisit completed lessons at any time</li> <li>Ctrl+Enter runs your code</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> </ul>
`; `;
elements.modalContainer.classList.remove("hidden"); elements.modalContainer.classList.remove("hidden");
} }
// Reset user progress function showResetConfirmation() {
function resetProgress() {
elements.modalTitle.textContent = "Reset Progress"; elements.modalTitle.textContent = "Reset Progress";
elements.modalContent.innerHTML = ` elements.modalContent.innerHTML = `
<p>Are you sure you want to reset all your progress? This cannot be undone.</p> <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;"> <div style="display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px;">
<button id="cancel-reset" class="btn">Cancel</button> <button id="cancel-reset" class="btn">Cancel</button>
<button id="confirm-reset" class="btn btn-primary">Reset Progress</button> <button id="confirm-reset" class="btn btn-ghost">Reset All</button>
</div> </div>
`; `;
document.getElementById("cancel-reset").addEventListener("click", closeModal); document.getElementById("cancel-reset").addEventListener("click", closeModal);
document.getElementById("confirm-reset").addEventListener("click", () => { document.getElementById("confirm-reset").addEventListener("click", () => {
localStorage.removeItem("codeCrispies.progress"); lessonEngine.clearProgress();
localStorage.removeItem("codeCrispies.lastModuleId");
state.userProgress = {};
closeModal(); closeModal();
closeSidebar();
// Reload the current module // Reload first module
if (state.currentModule) { const modules = lessonEngine.modules;
const currentModuleId = state.currentModule.id; if (modules.length > 0) {
selectModule(currentModuleId); selectModule(modules[0].id);
} else if (state.modules.length > 0) {
selectModule(state.modules[0].id);
} }
// Update progress indicator updateProgressDisplay();
updateModuleSelectorButtonProgress();
}); });
elements.modalContainer.classList.remove("hidden"); elements.modalContainer.classList.remove("hidden");
} }
// Close the modal
function closeModal() { function closeModal() {
elements.modalContainer.classList.add("hidden"); elements.modalContainer.classList.add("hidden");
} }
// Handle clicks in the code editor to focus the input // ================= KEYBOARD HANDLERS =================
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) { function handleTabKey(e) {
if (e.key === "Tab") { if (e.key === "Tab") {
e.preventDefault(); e.preventDefault();
@@ -568,103 +510,60 @@ function handleTabKey(e) {
const start = e.target.selectionStart; const start = e.target.selectionStart;
const end = e.target.selectionEnd; 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); 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; e.target.selectionStart = e.target.selectionEnd = start + 2;
} }
} }
// Initialize the application // ================= INITIALIZATION =================
function init() { function init() {
loadUserProgress();
loadUserSettings(); loadUserSettings();
initializeModules().catch(console.error); initializeModules().catch(console.error);
initFeedbackToggle();
// Event listeners // 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.prevBtn.addEventListener("click", prevLesson);
elements.nextBtn.addEventListener("click", nextLesson); elements.nextBtn.addEventListener("click", nextLesson);
elements.runBtn.addEventListener("click", runCode); elements.runBtn.addEventListener("click", runCode);
elements.modalClose.addEventListener("click", closeModal);
elements.moduleSelectorBtn.addEventListener("click", showModuleSelector); // Modals
elements.resetBtn.addEventListener("click", resetProgress);
elements.helpBtn.addEventListener("click", showHelp); elements.helpBtn.addEventListener("click", showHelp);
elements.codeInput.addEventListener("click", handleEditorClick); elements.modalClose.addEventListener("click", closeModal);
elements.resetBtn.addEventListener("click", showResetConfirmation);
// Also make the editor container clickable to focus the text area // Settings
elements.editorContent.addEventListener("click", (e) => {
elements.codeInput.focus();
});
// Load user settings
elements.disableFeedbackToggle.addEventListener("change", (e) => { elements.disableFeedbackToggle.addEventListener("change", (e) => {
state.userSettings.disableFeedbackErrors = !e.target.checked; state.userSettings.disableFeedbackErrors = !e.target.checked;
saveUserSettings(); saveUserSettings();
}); });
// Add tab key handler for the code input // Editor interactions
elements.codeInput.addEventListener("keydown", handleTabKey); elements.codeInput.addEventListener("keydown", handleTabKey);
elements.editorContent?.addEventListener("click", () => {
elements.codeInput.focus();
});
// Handle keyboard shortcuts // Keyboard shortcuts
document.addEventListener("keydown", (e) => { document.addEventListener("keydown", (e) => {
// Ctrl+Enter to run code // Ctrl+Enter to run code
if (e.ctrlKey && e.key === "Enter") { if (e.ctrlKey && e.key === "Enter") {
runCode(); runCode();
e.preventDefault(); e.preventDefault();
} }
});
// Add this to your app.js file // Escape to close sidebar
if (e.key === "Escape") {
// Mobile Menu Functionality closeSidebar();
document.addEventListener("DOMContentLoaded", function () { closeModal();
// Create hamburger menu button }
const hamburger = document.createElement("button");
hamburger.className = "hamburger";
hamburger.setAttribute("aria-label", "Toggle menu");
hamburger.innerHTML = `
<span class="hamburger-line"></span>
<span class="hamburger-line"></span>
<span class="hamburger-line"></span>
`;
// Get the header and nav elements
const header = document.querySelector(".header");
const logo = document.querySelector(".logo");
const nav = document.querySelector(".main-nav");
// Insert hamburger button after the logo
header.insertBefore(hamburger, logo.nextSibling);
// Toggle menu on hamburger click
hamburger.addEventListener("click", function () {
nav.classList.toggle("open");
hamburger.classList.toggle("open");
// Set aria-expanded attribute for accessibility
const isExpanded = nav.classList.contains("open");
hamburger.setAttribute("aria-expanded", isExpanded);
});
// Close menu when clicking outside
document.addEventListener("click", function (event) {
if (!nav.contains(event.target) && !hamburger.contains(event.target) && nav.classList.contains("open")) {
nav.classList.remove("open");
hamburger.classList.remove("open");
hamburger.setAttribute("aria-expanded", false);
}
});
// Close menu when window is resized to desktop size
window.addEventListener("resize", function () {
if (window.innerWidth > 768 && nav.classList.contains("open")) {
nav.classList.remove("open");
hamburger.classList.remove("open");
hamburger.setAttribute("aria-expanded", false);
}
});
}); });
} }

View File

@@ -9,104 +9,126 @@
</head> </head>
<body> <body>
<div class="app-container"> <div class="app-container">
<!-- Minimal Header -->
<header class="header"> <header class="header">
<button id="menu-btn" class="menu-toggle" aria-label="Open menu">
<span class="hamburger-line"></span>
<span class="hamburger-line"></span>
<span class="hamburger-line"></span>
</button>
<div class="logo"> <div class="logo">
<img src="./bowl.png" width="52" alt="CODE CRISPIES Logo" /> <img src="./bowl.png" width="40" alt="CODE CRISPIES Logo" />
<h1>CODE<br /><span>CRISPIES</span></h1> <h1>CODE<span>CRISPIES</span></h1>
</div> </div>
<nav class="main-nav"> <button id="help-btn" class="help-toggle" aria-label="Help">?</button>
<ul>
<li class="toggle-container">
<label class="toggle-switch" title="Toggle error feedback">
<input type="checkbox" id="disable-feedback-toggle" checked />
<span class="toggle-slider"></span>
<span class="toggle-label">Show Hints</span>
</label>
</li>
<li><button id="module-selector-btn" class="btn">Progress</button></li>
<li><button id="reset-btn" class="btn">Reset Progress</button></li>
<li><button id="help-btn" class="btn">Help</button></li>
</ul>
</nav>
</header> </header>
<main class="main-content"> <!-- Main Game Layout -->
<div class="sidebar"> <main class="game-layout">
<div class="module-list"> <!-- Left Panel: Instructions + Editor -->
<!-- Module list will be populated here --> <div class="left-panel">
</div> <section class="instructions">
<div class="lesson-progress"> <h2 id="lesson-title">Loading...</h2>
<!-- Lesson progress will be shown here --> <div class="lesson-description" id="lesson-description">
</div> Please select a lesson to begin.
</div>
<div class="task-instruction" id="task-instruction">
<!-- Task instructions will be shown here -->
</div>
</section>
<section class="editor-section">
<div class="code-editor">
<div class="editor-header">
<label for="code-input" class="editor-label">CSS Editor</label>
<div class="editor-actions">
<div class="validation-indicators-container"></div>
<button id="run-btn" class="btn btn-run">
<img src="./gear.svg" alt="" />Run
</button>
</div>
</div>
<div class="editor-content">
<textarea id="code-input" class="code-input" spellcheck="false"></textarea>
</div>
</div>
<div class="hint-area" id="hint-area">
<!-- Hints displayed inline here -->
</div>
</section>
</div> </div>
<div class="content-area"> <!-- Right Panel: Preview + Navigation -->
<div class="lesson-container"> <div class="right-panel">
<h2 id="lesson-title">Loading...</h2> <div class="preview-section">
<div class="lesson-description" id="lesson-description">Please select a lesson to begin.</div> <div class="preview-header">
<span class="preview-label">Your Output</span>
<div class="challenge-container"> <button id="show-expected-btn" class="btn btn-small">Show Expected</button>
<div class="preview-comparison" id="preview-comparison"> </div>
<div class="preview-pane preview-student"> <div class="preview-wrapper">
<div class="preview-header"> <div class="preview-frame" id="preview-area">
<span class="preview-label">Your Output</span> <!-- User's preview iframe will be shown here -->
</div>
<div class="preview-frame" id="preview-area">
<!-- Student's preview iframe will be shown here -->
</div>
</div>
<div class="preview-pane preview-expected">
<div class="preview-header">
<span class="preview-label">Expected Result</span>
</div>
<div class="preview-frame" id="preview-expected">
<!-- Expected result iframe will be shown here -->
</div>
</div>
<div class="preview-overlay" id="match-overlay">
<div class="match-celebration">Perfect Match!</div>
</div>
</div> </div>
<div class="expected-overlay" id="expected-overlay">
<div class="editor-container"> <div class="expected-frame" id="preview-expected">
<div class="task-instruction" id="task-instruction"> <!-- Expected result iframe (toggleable) -->
<!-- Task instructions will be shown here -->
</div>
<div class="code-editor">
<div class="editor-header">
<label for="code-input">CSS Editor</label>
<div class="validation-indicators-container"></div>
<button id="run-btn" class="btn btn-secondary"><img src="./gear.svg" alt="" />Run</button>
</div>
<div class="editor-content">
<!-- <pre><code id="editor-prefix"></code></pre>-->
<textarea id="code-input" class="code-input" spellcheck="false"></textarea>
<!-- <pre><code id="editor-suffix"></code></pre>-->
</div>
</div> </div>
</div> </div>
</div> </div>
</div>
<footer> <div class="game-controls">
Free and Open Source Software: <button id="prev-btn" class="btn">Previous</button>
<a href="https://github.com/nextlevelshit/code-crispies" target="_blank" <div class="level-indicator" id="level-indicator">Level 0/0</div>
>https://github.com/nextlevelshit/code-crispies</a <button id="next-btn" class="btn btn-primary">Next</button>
>
by <a href="https://dailysh.it" title="Website of Michael W. Czechowski">Michael W. Czechowski</a>
</footer>
<div class="controls">
<button id="prev-btn" class="btn">Previous</button>
<div class="level-indicator" id="level-indicator">Level 0/0</div>
<button id="next-btn" class="btn btn-primary">Next</button>
</div>
</div> </div>
</div> </div>
</main> </main>
<!-- Sidebar Backdrop -->
<div class="sidebar-backdrop" id="sidebar-backdrop"></div>
<!-- Slide-out Sidebar -->
<aside class="sidebar-drawer" id="sidebar-drawer">
<div class="sidebar-header">
<h3>Menu</h3>
<button id="close-sidebar" class="close-btn" aria-label="Close menu">&times;</button>
</div>
<div class="sidebar-section">
<h4>Progress</h4>
<div class="progress-display" id="progress-display">
<div class="progress-bar">
<div class="progress-fill" id="progress-fill"></div>
</div>
<span class="progress-text" id="progress-text">0% Complete</span>
</div>
</div>
<div class="sidebar-section">
<h4>Lessons</h4>
<div class="module-list" id="module-list">
<!-- Module list will be populated here -->
</div>
</div>
<div class="sidebar-section">
<h4>Settings</h4>
<label class="toggle-switch">
<input type="checkbox" id="disable-feedback-toggle" checked />
<span class="toggle-slider"></span>
<span class="toggle-label">Show Hints</span>
</label>
<button id="reset-btn" class="btn btn-text">Reset All Progress</button>
</div>
<footer class="app-footer">
Open Source:
<a href="https://github.com/nextlevelshit/code-crispies" target="_blank">GitHub</a>
by <a href="https://dailysh.it" title="Michael W. Czechowski">mwc</a>
</footer>
</aside>
<!-- Help Modal -->
<div id="modal-container" class="modal-container hidden"> <div id="modal-container" class="modal-container hidden">
<div class="modal"> <div class="modal">
<div class="modal-header"> <div class="modal-header">

File diff suppressed because it is too large Load Diff