feat: add JavaScript learning section with starter lessons and sidebar section headers

Implementation following plan:
- S01: Foundation: schema, section config, and router
- S02: Install CodeMirror JavaScript language support
- S03: Create JavaScript lesson JSON files (variables, DOM, events)
- S04: Register JavaScript lessons in module stores
- S05: Add JavaScript validation logic
- S06: Add JavaScript mode to LessonEngine preview rendering
- S07: Add JavaScript mode to CodeEditor
- S08: Update app.js for JavaScript mode support
- S09: Update navigation HTML and CSS theming for JavaScript section
- S10: Add section grouping headers in sidebar navigation
- S11: Update and write tests
This commit is contained in:
2026-03-28 20:22:50 +01:00
parent 372320b807
commit ae8f9fef45
20 changed files with 863 additions and 27 deletions

View File

@@ -2,6 +2,7 @@
* Renderer - Handles UI updates for the CSS learning platform
*/
import { t } from "../i18n.js";
import { getModuleSection, getSection, getSectionList } from "../config/sections.js";
/**
* Compute lesson difficulty based on lesson structure
@@ -72,8 +73,24 @@ export function renderModuleList(container, modules, onSelectModule, onSelectLes
}
}
// Group modules by section for headers
let currentSectionId = null;
// Create list items for each module
modules.forEach((module) => {
// Insert section header when section changes
const sectionId = getModuleSection(module);
if (sectionId !== currentSectionId && !module.excludeFromProgress) {
currentSectionId = sectionId;
const section = getSection(sectionId);
if (section) {
const header = document.createElement("h3");
header.className = "sidebar-section-header";
header.textContent = section.title;
header.style.borderLeftColor = section.color;
container.appendChild(header);
}
}
// Create module container
// Use native <details>/<summary> for expand/collapse
const moduleContainer = document.createElement("details");

View File

@@ -27,7 +27,7 @@ export const RouteType = {
/**
* Valid section IDs
*/
const SECTIONS = ["css", "html", "markdown"]; // tailwind temporarily disabled
const SECTIONS = ["css", "html", "markdown", "javascript"]; // tailwind temporarily disabled
/**
* Valid language codes for URL-based switching

View File

@@ -10,6 +10,8 @@ export function validateUserCode(userCode, lesson) {
return validateHtmlCode(userCode, lesson);
case "tailwind":
return validateTailwindClasses(userCode, lesson);
case "javascript":
return validateJavaScriptCode(userCode, lesson);
case "css":
default:
return validateCssCode(userCode, lesson);
@@ -204,6 +206,80 @@ function validateHtmlCode(userHtml, lesson) {
return result;
}
/**
* Validate user JavaScript code against the lesson requirements
* @param {string} userCode - User submitted JavaScript code
* @param {Object} lesson - The current lesson object
* @returns {Object} Validation result with isValid and message properties
*/
function validateJavaScriptCode(userCode, lesson) {
if (!lesson || !lesson.validations) {
return { isValid: true, message: "No validations specified for this lesson." };
}
const validations = lesson.validations;
let result = {
isValid: true,
validCases: 0,
totalCases: validations.length,
message: "Your CODE looks CRISPY!"
};
for (const validation of validations) {
const { type, value, message, options } = validation;
let validationPassed = false;
switch (type) {
case "contains":
validationPassed = containsValidation(userCode, value, options);
if (!validationPassed) {
result = {
...result,
isValid: false,
message: message || `Your code should include "${value}".`
};
}
break;
case "not_contains":
validationPassed = !containsValidation(userCode, value, options);
if (!validationPassed) {
result = {
...result,
isValid: false,
message: message || `Your code should not include "${value}".`
};
}
break;
case "regex":
validationPassed = regexValidation(userCode, value, options);
if (!validationPassed) {
result = {
...result,
isValid: false,
message: message || "Your code does not match the expected pattern."
};
}
break;
default:
console.warn(`Unknown JavaScript validation type: ${type}`);
validationPassed = true;
}
if (validationPassed) {
result.validCases++;
} else {
return result;
}
}
result.validCases = validations.length;
return result;
}
function validateTailwindClasses(userClasses, lesson) {
if (!lesson || !lesson.validations) {
return { isValid: true, message: "No validations specified for this lesson." };