feat: add JavaScript lesson section with starter lessons and sidebar section grouping headers
Implementation following plan: - S01: Update JSON schema to support 'javascript' mode - S02: Install @codemirror/lang-javascript dependency - S03: Define JavaScript section in sections.js - S04: Create 3 JavaScript lesson JSON files (variables, DOM, events) - S05: Add JavaScript validation support in validator.js - S06: Add JavaScript preview rendering in LessonEngine.js - S07: Add JavaScript CodeMirror mode and editor config - S08: Register JavaScript modules in all language stores - S09: Add JavaScript section to landing page, navigation, and app config - S10: Add sidebar section grouping headers with category mapping - S11: Update tests for JavaScript mode and section headers
This commit is contained in:
98
lessons/50-js-variables.json
Normal file
98
lessons/50-js-variables.json
Normal file
@@ -0,0 +1,98 @@
|
||||
{
|
||||
"$schema": "../schemas/code-crispies-module-schema.json",
|
||||
"id": "js-variables",
|
||||
"title": "JS Variables",
|
||||
"description": "Learn to declare variables with let and const, work with strings and numbers, and use template literals to build dynamic text.",
|
||||
"mode": "javascript",
|
||||
"difficulty": "beginner",
|
||||
"lessons": [
|
||||
{
|
||||
"id": "js-const",
|
||||
"title": "Constants",
|
||||
"description": "Use <kbd>const</kbd> to declare a variable that cannot be reassigned. Constants are great for values that stay the same throughout your program.",
|
||||
"task": "Declare a constant named <kbd>name</kbd> with the value <kbd>\"Ada\"</kbd>",
|
||||
"previewHTML": "<p id=\"out\">Waiting...</p>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; }",
|
||||
"sandboxCSS": "",
|
||||
"initialCode": "",
|
||||
"solution": "const name = \"Ada\";",
|
||||
"codePrefix": "",
|
||||
"codeSuffix": "\ndocument.getElementById(\"out\").textContent = name;",
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "const",
|
||||
"message": "Use <kbd>const</kbd> to declare the variable"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "const\\s+name\\s*=",
|
||||
"message": "Name your constant <kbd>name</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "[\"']Ada[\"']",
|
||||
"message": "Set the value to <kbd>\"Ada\"</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "js-let",
|
||||
"title": "Let Variables",
|
||||
"description": "Use <kbd>let</kbd> to declare a variable that can be reassigned later. This is useful when you need to update a value.",
|
||||
"task": "Declare a variable <kbd>score</kbd> with <kbd>let</kbd> and set it to <kbd>0</kbd>",
|
||||
"previewHTML": "<p id=\"out\">Waiting...</p>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; }",
|
||||
"sandboxCSS": "",
|
||||
"initialCode": "",
|
||||
"solution": "let score = 0;",
|
||||
"codePrefix": "",
|
||||
"codeSuffix": "\ndocument.getElementById(\"out\").textContent = \"Score: \" + score;",
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "let",
|
||||
"message": "Use <kbd>let</kbd> to declare the variable"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "let\\s+score\\s*=\\s*0",
|
||||
"message": "Set <kbd>score</kbd> to <kbd>0</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "js-template",
|
||||
"title": "Template Literals",
|
||||
"description": "Template literals use backticks <kbd>`</kbd> and <kbd>${}</kbd> to embed expressions inside strings. They make building dynamic text much easier than string concatenation.",
|
||||
"task": "Create a <kbd>const msg</kbd> using a template literal: <kbd>`Hi, ${name}!`</kbd>",
|
||||
"previewHTML": "<p id=\"out\">Waiting...</p>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; }",
|
||||
"sandboxCSS": "",
|
||||
"initialCode": "",
|
||||
"solution": "const msg = `Hi, ${name}!`;",
|
||||
"codePrefix": "const name = \"Ada\";\n",
|
||||
"codeSuffix": "\ndocument.getElementById(\"out\").textContent = msg;",
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "${",
|
||||
"message": "Use <kbd>${}</kbd> to embed the variable inside the template"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "`[^`]*\\$\\{\\s*name\\s*\\}[^`]*`",
|
||||
"message": "Embed <kbd>name</kbd> inside a template literal with backticks"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "const\\s+msg\\s*=",
|
||||
"message": "Assign the result to a constant named <kbd>msg</kbd>"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
93
lessons/51-js-dom.json
Normal file
93
lessons/51-js-dom.json
Normal file
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"$schema": "../schemas/code-crispies-module-schema.json",
|
||||
"id": "js-dom",
|
||||
"title": "JS DOM",
|
||||
"description": "Learn to select HTML elements with querySelector, change their text content, and modify their styles using JavaScript.",
|
||||
"mode": "javascript",
|
||||
"difficulty": "beginner",
|
||||
"lessons": [
|
||||
{
|
||||
"id": "js-query",
|
||||
"title": "Select an Element",
|
||||
"description": "Use <kbd>document.querySelector()</kbd> to find an element by its CSS selector. It returns the first matching element.",
|
||||
"task": "Select the element with id <kbd>box</kbd> and store it in a <kbd>const el</kbd>",
|
||||
"previewHTML": "<div id=\"box\" style=\"width:80px;height:80px;background:coral;border-radius:8px;\"></div>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; }",
|
||||
"sandboxCSS": "",
|
||||
"initialCode": "",
|
||||
"solution": "const el = document.querySelector(\"#box\");",
|
||||
"codePrefix": "",
|
||||
"codeSuffix": "\nif (el) el.textContent = \"Found!\";",
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "querySelector",
|
||||
"message": "Use <kbd>document.querySelector()</kbd> to select the element"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "querySelector\\s*\\([\"']#box[\"']\\)",
|
||||
"message": "Pass <kbd>\"#box\"</kbd> as the selector"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "const\\s+el\\s*=",
|
||||
"message": "Store the result in a constant named <kbd>el</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "js-text",
|
||||
"title": "Change Text",
|
||||
"description": "The <kbd>textContent</kbd> property lets you read or change the text inside an element. Setting it replaces all the element's text.",
|
||||
"task": "Set the <kbd>textContent</kbd> of <kbd>el</kbd> to <kbd>\"Hello!\"</kbd>",
|
||||
"previewHTML": "<p id=\"msg\">Old text</p>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; }",
|
||||
"sandboxCSS": "",
|
||||
"initialCode": "",
|
||||
"solution": "el.textContent = \"Hello!\";",
|
||||
"codePrefix": "const el = document.querySelector(\"#msg\");\n",
|
||||
"codeSuffix": "",
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "textContent",
|
||||
"message": "Use the <kbd>textContent</kbd> property"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "el\\.textContent\\s*=\\s*[\"']Hello![\"']",
|
||||
"message": "Set <kbd>el.textContent</kbd> to <kbd>\"Hello!\"</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "js-style",
|
||||
"title": "Change Style",
|
||||
"description": "Access an element's inline styles through the <kbd>style</kbd> property. CSS properties use camelCase in JavaScript, so <kbd>background-color</kbd> becomes <kbd>backgroundColor</kbd>.",
|
||||
"task": "Set <kbd>el.style.backgroundColor</kbd> to <kbd>\"gold\"</kbd>",
|
||||
"previewHTML": "<div id=\"box\" style=\"width:80px;height:80px;background:coral;border-radius:8px;\"></div>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; }",
|
||||
"sandboxCSS": "",
|
||||
"initialCode": "",
|
||||
"solution": "el.style.backgroundColor = \"gold\";",
|
||||
"codePrefix": "const el = document.querySelector(\"#box\");\n",
|
||||
"codeSuffix": "",
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "style.backgroundColor",
|
||||
"message": "Use <kbd>el.style.backgroundColor</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "\\.style\\.backgroundColor\\s*=\\s*[\"']gold[\"']",
|
||||
"message": "Set the background color to <kbd>\"gold\"</kbd>"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
103
lessons/52-js-events.json
Normal file
103
lessons/52-js-events.json
Normal file
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"$schema": "../schemas/code-crispies-module-schema.json",
|
||||
"id": "js-events",
|
||||
"title": "JS Events",
|
||||
"description": "Learn to respond to user actions by adding event listeners for clicks, toggling classes, and updating the page dynamically.",
|
||||
"mode": "javascript",
|
||||
"difficulty": "beginner",
|
||||
"lessons": [
|
||||
{
|
||||
"id": "js-click",
|
||||
"title": "Click Handler",
|
||||
"description": "Use <kbd>addEventListener(\"click\", ...)</kbd> to run code when an element is clicked. The first argument is the event type and the second is a callback function.",
|
||||
"task": "Add a <kbd>click</kbd> event listener to <kbd>btn</kbd> that sets <kbd>out.textContent</kbd> to <kbd>\"Clicked!\"</kbd>",
|
||||
"previewHTML": "<button id=\"btn\" style=\"padding:8px 16px;font-size:1rem;\">Click me</button>\n<p id=\"out\">Waiting...</p>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; }",
|
||||
"sandboxCSS": "",
|
||||
"initialCode": "",
|
||||
"solution": "btn.addEventListener(\"click\", () => {\n out.textContent = \"Clicked!\";\n});",
|
||||
"codePrefix": "const btn = document.querySelector(\"#btn\");\nconst out = document.querySelector(\"#out\");\n",
|
||||
"codeSuffix": "",
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "addEventListener",
|
||||
"message": "Use <kbd>addEventListener</kbd> to listen for events"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "addEventListener\\s*\\(\\s*[\"']click[\"']",
|
||||
"message": "Listen for the <kbd>\"click\"</kbd> event"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "textContent\\s*=\\s*[\"']Clicked![\"']",
|
||||
"message": "Set <kbd>out.textContent</kbd> to <kbd>\"Clicked!\"</kbd> inside the handler"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "js-toggle",
|
||||
"title": "Toggle a Class",
|
||||
"description": "Use <kbd>classList.toggle()</kbd> to add a class if it's missing or remove it if it's present. This is perfect for on/off states like toggling dark mode or active states.",
|
||||
"task": "Inside the click handler, call <kbd>box.classList.toggle(\"on\")</kbd>",
|
||||
"previewHTML": "<div id=\"box\" style=\"width:80px;height:80px;background:coral;border-radius:8px;transition:background 0.3s;\"></div>\n<style>.on { background: mediumseagreen !important; }</style>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; }",
|
||||
"sandboxCSS": "",
|
||||
"initialCode": "",
|
||||
"solution": "box.addEventListener(\"click\", () => {\n box.classList.toggle(\"on\");\n});",
|
||||
"codePrefix": "const box = document.querySelector(\"#box\");\n",
|
||||
"codeSuffix": "",
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "classList.toggle",
|
||||
"message": "Use <kbd>classList.toggle()</kbd> to toggle the class"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "classList\\.toggle\\s*\\(\\s*[\"']on[\"']\\s*\\)",
|
||||
"message": "Toggle the class <kbd>\"on\"</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "addEventListener",
|
||||
"message": "Use <kbd>addEventListener</kbd> to listen for clicks"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "js-counter",
|
||||
"title": "Simple Counter",
|
||||
"description": "Combine variables and event listeners to build interactive features. Use <kbd>let</kbd> for a value that changes, and update the display each time the button is clicked.",
|
||||
"task": "In the click handler, increment <kbd>count</kbd> by 1 and set <kbd>out.textContent</kbd> to <kbd>count</kbd>",
|
||||
"previewHTML": "<button id=\"btn\" style=\"padding:8px 16px;font-size:1rem;\">Add 1</button>\n<p id=\"out\">0</p>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; }",
|
||||
"sandboxCSS": "",
|
||||
"initialCode": "",
|
||||
"solution": "btn.addEventListener(\"click\", () => {\n count++;\n out.textContent = count;\n});",
|
||||
"codePrefix": "const btn = document.querySelector(\"#btn\");\nconst out = document.querySelector(\"#out\");\nlet count = 0;\n",
|
||||
"codeSuffix": "",
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "addEventListener",
|
||||
"message": "Use <kbd>addEventListener</kbd> to listen for clicks"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "count\\s*\\+\\+|count\\s*\\+=\\s*1|count\\s*=\\s*count\\s*\\+\\s*1",
|
||||
"message": "Increment <kbd>count</kbd> by 1 (use <kbd>count++</kbd>)"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "out\\.textContent\\s*=\\s*count",
|
||||
"message": "Update the display with <kbd>out.textContent = count</kbd>"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user