Merge pull request 'feat: add JavaScript learning section with starter lessons and sidebar section headers' (#8) from feat/impl-feature-20260328-200101-ecae into main

This commit is contained in:
2026-03-28 21:32:38 +01:00
20 changed files with 863 additions and 27 deletions

View File

@@ -0,0 +1,139 @@
{
"$schema": "../schemas/code-crispies-module-schema.json",
"id": "js-variables",
"title": "JS Variables",
"description": "Learn to declare variables with let and const, and work with basic data types in JavaScript.",
"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 the default choice for most values in modern JavaScript.",
"task": "Declare a constant named <kbd>name</kbd> with the value <kbd>\"Alice\"</kbd>",
"previewHTML": "<p id=\"out\">Waiting...</p>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; }",
"sandboxCSS": "",
"initialCode": "",
"codePrefix": "",
"codeSuffix": "\ndocument.getElementById('out').textContent = name;",
"solution": "const name = \"Alice\";",
"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": "Declare a constant called <kbd>name</kbd>"
},
{
"type": "regex",
"value": "\"Alice\"|'Alice'|`Alice`",
"message": "Set the value to <kbd>\"Alice\"</kbd>"
}
]
},
{
"id": "js-let",
"title": "Let Variables",
"description": "Use <kbd>let</kbd> to declare variables that you plan to reassign later. Unlike <kbd>const</kbd>, a <kbd>let</kbd> variable can change its value.",
"task": "Declare a variable <kbd>count</kbd> with <kbd>let</kbd> set to <kbd>0</kbd>, then reassign it to <kbd>5</kbd>",
"previewHTML": "<p id=\"out\">Waiting...</p>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; }",
"sandboxCSS": "",
"initialCode": "",
"codePrefix": "",
"codeSuffix": "\ndocument.getElementById('out').textContent = count;",
"solution": "let count = 0;\ncount = 5;",
"previewContainer": "preview-area",
"validations": [
{
"type": "regex",
"value": "let\\s+count\\s*=\\s*0",
"message": "Start with <kbd>let count = 0;</kbd>"
},
{
"type": "regex",
"value": "count\\s*=\\s*5",
"message": "Reassign count to <kbd>5</kbd>"
}
]
},
{
"id": "js-string",
"title": "Template Literals",
"description": "Template literals use backticks <kbd>`</kbd> and <kbd>${}</kbd> to embed expressions inside strings. This makes building dynamic text much easier than string concatenation.",
"task": "Create a constant <kbd>msg</kbd> using a template literal: <kbd>`Hello, ${name}!`</kbd>",
"previewHTML": "<p id=\"out\">Waiting...</p>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; }",
"sandboxCSS": "",
"initialCode": "",
"codePrefix": "const name = \"World\";\n",
"codeSuffix": "\ndocument.getElementById('out').textContent = msg;",
"solution": "const msg = `Hello, ${name}!`;",
"previewContainer": "preview-area",
"validations": [
{
"type": "regex",
"value": "const\\s+msg\\s*=",
"message": "Declare a constant called <kbd>msg</kbd>"
},
{
"type": "contains",
"value": "${name}",
"message": "Use <kbd>${name}</kbd> inside backticks to embed the variable"
},
{
"type": "regex",
"value": "`.*\\$\\{name\\}.*`",
"message": "Wrap the whole string in backticks <kbd>`</kbd>"
}
]
},
{
"id": "js-array",
"title": "Arrays",
"description": "Arrays store ordered lists of values in square brackets. Access items by index (starting at 0) and use <kbd>.length</kbd> to get the count.",
"task": "Create a constant <kbd>colors</kbd> with an array: <kbd>[\"red\", \"green\", \"blue\"]</kbd>",
"previewHTML": "<p id=\"out\">Waiting...</p>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; }",
"sandboxCSS": "",
"initialCode": "",
"codePrefix": "",
"codeSuffix": "\ndocument.getElementById('out').textContent = colors.join(', ');",
"solution": "const colors = [\"red\", \"green\", \"blue\"];",
"previewContainer": "preview-area",
"validations": [
{
"type": "regex",
"value": "const\\s+colors\\s*=",
"message": "Declare a constant called <kbd>colors</kbd>"
},
{
"type": "contains",
"value": "[",
"message": "Use square brackets <kbd>[</kbd> to create an array"
},
{
"type": "regex",
"value": "(\"red\"|'red'|`red`)",
"message": "Include <kbd>\"red\"</kbd> in the array"
},
{
"type": "regex",
"value": "(\"green\"|'green'|`green`)",
"message": "Include <kbd>\"green\"</kbd> in the array"
},
{
"type": "regex",
"value": "(\"blue\"|'blue'|`blue`)",
"message": "Include <kbd>\"blue\"</kbd> in the array"
}
]
}
]
}

139
lessons/51-js-dom.json Normal file
View File

@@ -0,0 +1,139 @@
{
"$schema": "../schemas/code-crispies-module-schema.json",
"id": "js-dom",
"title": "JS DOM",
"description": "Learn to select and modify HTML elements using JavaScript DOM methods like querySelector and textContent.",
"mode": "javascript",
"difficulty": "beginner",
"lessons": [
{
"id": "js-query",
"title": "querySelector",
"description": "Use <kbd>document.querySelector()</kbd> to find the first element matching a CSS selector. It returns a single element you can then modify.",
"task": "Select the <kbd>h1</kbd> element and store it in a constant called <kbd>title</kbd>",
"previewHTML": "<h1>Hello</h1><p id=\"out\">Waiting...</p>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; }",
"sandboxCSS": "",
"initialCode": "",
"codePrefix": "",
"codeSuffix": "\ndocument.getElementById('out').textContent = title.tagName;",
"solution": "const title = document.querySelector('h1');",
"previewContainer": "preview-area",
"validations": [
{
"type": "contains",
"value": "querySelector",
"message": "Use <kbd>document.querySelector()</kbd> to select an element"
},
{
"type": "regex",
"value": "querySelector\\(['\"`]h1['\"`]\\)",
"message": "Pass <kbd>'h1'</kbd> as the selector"
},
{
"type": "regex",
"value": "const\\s+title\\s*=",
"message": "Store the result in a constant called <kbd>title</kbd>"
}
]
},
{
"id": "js-text",
"title": "textContent",
"description": "The <kbd>textContent</kbd> property lets you read or change the text inside an element. Setting it replaces all existing text.",
"task": "Select the <kbd>.msg</kbd> element and set its <kbd>textContent</kbd> to <kbd>\"Done!\"</kbd>",
"previewHTML": "<p class=\"msg\">Waiting...</p>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; }",
"sandboxCSS": "",
"initialCode": "",
"codePrefix": "",
"codeSuffix": "",
"solution": "document.querySelector('.msg').textContent = \"Done!\";",
"previewContainer": "preview-area",
"validations": [
{
"type": "contains",
"value": "querySelector",
"message": "Use <kbd>querySelector</kbd> to find the element"
},
{
"type": "contains",
"value": "textContent",
"message": "Use the <kbd>textContent</kbd> property to change the text"
},
{
"type": "regex",
"value": "(\"Done!\"|'Done!'|`Done!`)",
"message": "Set the text to <kbd>\"Done!\"</kbd>"
}
]
},
{
"id": "js-style",
"title": "Inline Styles",
"description": "Access the <kbd>style</kbd> property to set inline CSS on an element. CSS properties with dashes become camelCase: <kbd>background-color</kbd> becomes <kbd>backgroundColor</kbd>.",
"task": "Select the <kbd>.box</kbd> element and set its <kbd>style.color</kbd> to <kbd>\"coral\"</kbd>",
"previewHTML": "<p class=\"box\">Style me!</p>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { font-size: 1.5rem; font-weight: bold; }",
"sandboxCSS": "",
"initialCode": "",
"codePrefix": "",
"codeSuffix": "",
"solution": "document.querySelector('.box').style.color = \"coral\";",
"previewContainer": "preview-area",
"validations": [
{
"type": "contains",
"value": "querySelector",
"message": "Use <kbd>querySelector</kbd> to find the element"
},
{
"type": "contains",
"value": ".style.",
"message": "Use the <kbd>.style</kbd> property to set CSS"
},
{
"type": "regex",
"value": "style\\.color\\s*=",
"message": "Set <kbd>style.color</kbd> on the element"
},
{
"type": "regex",
"value": "(\"coral\"|'coral'|`coral`)",
"message": "Set the color to <kbd>\"coral\"</kbd>"
}
]
},
{
"id": "js-classlist",
"title": "classList",
"description": "The <kbd>classList</kbd> property provides methods to add, remove, or toggle CSS classes on an element without touching other classes.",
"task": "Select the <kbd>.card</kbd> element and add the class <kbd>\"active\"</kbd> using <kbd>classList.add()</kbd>",
"previewHTML": "<div class=\"card\">Toggle me</div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .card { padding: 1rem; border: 2px solid gray; border-radius: 8px; } .active { border-color: coral; background: #fff0ee; }",
"sandboxCSS": "",
"initialCode": "",
"codePrefix": "",
"codeSuffix": "",
"solution": "document.querySelector('.card').classList.add(\"active\");",
"previewContainer": "preview-area",
"validations": [
{
"type": "contains",
"value": "classList",
"message": "Use the <kbd>classList</kbd> property"
},
{
"type": "regex",
"value": "classList\\.add\\(",
"message": "Call <kbd>classList.add()</kbd> to add a class"
},
{
"type": "regex",
"value": "(\"active\"|'active'|`active`)",
"message": "Add the class <kbd>\"active\"</kbd>"
}
]
}
]
}

118
lessons/52-js-events.json Normal file
View File

@@ -0,0 +1,118 @@
{
"$schema": "../schemas/code-crispies-module-schema.json",
"id": "js-events",
"title": "JS Events",
"description": "Learn to respond to user interactions with addEventListener for clicks, input changes, and keyboard events.",
"mode": "javascript",
"difficulty": "beginner",
"lessons": [
{
"id": "js-click",
"title": "Click Events",
"description": "Use <kbd>addEventListener('click', ...)</kbd> to run code when a user clicks an element. The first argument is the event name, the second is a callback function.",
"task": "Add a click listener to the <kbd>.btn</kbd> element that sets the <kbd>.msg</kbd> text to <kbd>\"Clicked!\"</kbd>",
"previewHTML": "<button class=\"btn\">Click me</button><p class=\"msg\">Waiting...</p>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .btn { padding: 0.5rem 1rem; border: none; background: steelblue; color: white; border-radius: 4px; cursor: pointer; }",
"sandboxCSS": "",
"initialCode": "",
"codePrefix": "const btn = document.querySelector('.btn');\nconst msg = document.querySelector('.msg');\n\n",
"codeSuffix": "",
"solution": "btn.addEventListener('click', () => {\n msg.textContent = \"Clicked!\";\n});",
"previewContainer": "preview-area",
"validations": [
{
"type": "contains",
"value": "addEventListener",
"message": "Use <kbd>addEventListener</kbd> to listen for events"
},
{
"type": "regex",
"value": "addEventListener\\(['\"`]click['\"`]",
"message": "Listen for the <kbd>'click'</kbd> event"
},
{
"type": "contains",
"value": "textContent",
"message": "Use <kbd>textContent</kbd> to update the text"
},
{
"type": "regex",
"value": "(\"Clicked!\"|'Clicked!'|`Clicked!`)",
"message": "Set the text to <kbd>\"Clicked!\"</kbd>"
}
]
},
{
"id": "js-toggle",
"title": "Toggle Classes",
"description": "Combine events with <kbd>classList.toggle()</kbd> to switch a class on and off. Each click adds the class if missing, or removes it if present.",
"task": "Add a click listener to <kbd>.btn</kbd> that toggles the class <kbd>\"on\"</kbd> on <kbd>.lamp</kbd>",
"previewHTML": "<button class=\"btn\">Toggle</button><div class=\"lamp\">💡</div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; text-align: center; } .btn { padding: 0.5rem 1rem; border: none; background: steelblue; color: white; border-radius: 4px; cursor: pointer; } .lamp { font-size: 3rem; margin-top: 1rem; opacity: 0.3; transition: opacity 0.3s; } .lamp.on { opacity: 1; }",
"sandboxCSS": "",
"initialCode": "",
"codePrefix": "const btn = document.querySelector('.btn');\nconst lamp = document.querySelector('.lamp');\n\n",
"codeSuffix": "",
"solution": "btn.addEventListener('click', () => {\n lamp.classList.toggle('on');\n});",
"previewContainer": "preview-area",
"validations": [
{
"type": "contains",
"value": "addEventListener",
"message": "Use <kbd>addEventListener</kbd> to listen for events"
},
{
"type": "regex",
"value": "addEventListener\\(['\"`]click['\"`]",
"message": "Listen for the <kbd>'click'</kbd> event"
},
{
"type": "regex",
"value": "classList\\.toggle\\(",
"message": "Use <kbd>classList.toggle()</kbd> to switch the class"
},
{
"type": "regex",
"value": "(\"on\"|'on'|`on`)",
"message": "Toggle the class <kbd>\"on\"</kbd>"
}
]
},
{
"id": "js-input",
"title": "Input Events",
"description": "The <kbd>input</kbd> event fires every time the value of an input field changes. Use <kbd>event.target.value</kbd> to read the current value.",
"task": "Add an input listener to <kbd>.field</kbd> that sets <kbd>.out</kbd> text to the input's value",
"previewHTML": "<input class=\"field\" placeholder=\"Type here...\"><p class=\"out\">Echo: </p>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .field { padding: 0.5rem; border: 2px solid #ccc; border-radius: 4px; font-size: 1rem; width: 100%; box-sizing: border-box; }",
"sandboxCSS": "",
"initialCode": "",
"codePrefix": "const field = document.querySelector('.field');\nconst out = document.querySelector('.out');\n\n",
"codeSuffix": "",
"solution": "field.addEventListener('input', (event) => {\n out.textContent = event.target.value;\n});",
"previewContainer": "preview-area",
"validations": [
{
"type": "contains",
"value": "addEventListener",
"message": "Use <kbd>addEventListener</kbd> to listen for events"
},
{
"type": "regex",
"value": "addEventListener\\(['\"`]input['\"`]",
"message": "Listen for the <kbd>'input'</kbd> event"
},
{
"type": "contains",
"value": "textContent",
"message": "Use <kbd>textContent</kbd> to update the output"
},
{
"type": "regex",
"value": "(event|e|evt)\\.target\\.value",
"message": "Read the input value with <kbd>event.target.value</kbd>"
}
]
}
]
}