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:
139
lessons/50-js-variables.json
Normal file
139
lessons/50-js-variables.json
Normal 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
139
lessons/51-js-dom.json
Normal 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
118
lessons/52-js-events.json
Normal 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>"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
7
package-lock.json
generated
7
package-lock.json
generated
@@ -13,6 +13,7 @@
|
||||
"@codemirror/commands": "^6.10.1",
|
||||
"@codemirror/lang-css": "^6.3.1",
|
||||
"@codemirror/lang-html": "^6.4.11",
|
||||
"@codemirror/lang-javascript": "^6.2.5",
|
||||
"@codemirror/lang-markdown": "^6.5.0",
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/theme-one-dark": "^6.1.3",
|
||||
@@ -208,9 +209,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-javascript": {
|
||||
"version": "6.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz",
|
||||
"integrity": "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==",
|
||||
"version": "6.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.5.tgz",
|
||||
"integrity": "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"@codemirror/commands": "^6.10.1",
|
||||
"@codemirror/lang-css": "^6.3.1",
|
||||
"@codemirror/lang-html": "^6.4.11",
|
||||
"@codemirror/lang-javascript": "^6.2.5",
|
||||
"@codemirror/lang-markdown": "^6.5.0",
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/theme-one-dark": "^6.1.3",
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
},
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": ["css", "tailwind", "html", "markdown"],
|
||||
"description": "Whether this module teaches CSS, Tailwind, HTML, or Markdown"
|
||||
"enum": ["css", "tailwind", "html", "markdown", "javascript"],
|
||||
"description": "Whether this module teaches CSS, Tailwind, HTML, Markdown, or JavaScript"
|
||||
},
|
||||
"difficulty": {
|
||||
"type": "string",
|
||||
@@ -60,7 +60,7 @@
|
||||
},
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": ["css", "tailwind", "html", "markdown"],
|
||||
"enum": ["css", "tailwind", "html", "markdown", "javascript"],
|
||||
"description": "Override module mode for individual lessons"
|
||||
},
|
||||
"tailwindConfig": {
|
||||
|
||||
67
src/app.js
67
src/app.js
@@ -578,6 +578,11 @@ function updateEditorForMode(mode) {
|
||||
label: "Markdown Editor",
|
||||
cmMode: "markdown"
|
||||
},
|
||||
javascript: {
|
||||
placeholder: "// Write your JavaScript here...",
|
||||
label: "JavaScript Editor",
|
||||
cmMode: "javascript"
|
||||
},
|
||||
playground: {
|
||||
placeholder: "<style>\n /* CSS here */\n</style>\n\n<!-- HTML here -->",
|
||||
label: "HTML & CSS",
|
||||
@@ -1493,6 +1498,64 @@ This is \`inline code\`.</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
javascript: `
|
||||
<div class="section-overview">
|
||||
<p><strong>JavaScript</strong> is the programming language of the web. It adds interactivity to HTML pages—responding to clicks, updating content dynamically, validating forms, and much more. Every modern browser includes a JavaScript engine, making it the most widely deployed programming language in the world.</p>
|
||||
<p>These beginner lessons cover the fundamentals: declaring variables, selecting and modifying DOM elements, and handling user events. Each concept builds on the previous one, giving you the tools to make any web page interactive.</p>
|
||||
</div>
|
||||
|
||||
<div class="topic-row">
|
||||
<div class="topic-text">
|
||||
<h2>Variables & Data Types</h2>
|
||||
<p>JavaScript uses <code>const</code> for values that won't change and <code>let</code> for values that will. Template literals with backticks make it easy to embed expressions in strings using <code>\${...}</code> syntax.</p>
|
||||
<p>Arrays store ordered collections in square brackets. Objects store key-value pairs in curly braces. These are the building blocks of every JavaScript program.</p>
|
||||
<a href="#js-variables/0" class="topic-link">Learn JS Variables</a>
|
||||
</div>
|
||||
<div class="topic-code">
|
||||
<div class="code-block">
|
||||
<pre><code>const name = "Alice";
|
||||
let count = 0;
|
||||
count = count + 1;
|
||||
|
||||
const msg = \`Hello, \${name}!\`;
|
||||
const colors = ["red", "green"];</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="topic-row">
|
||||
<div class="topic-text">
|
||||
<h2>DOM Manipulation</h2>
|
||||
<p>The DOM (Document Object Model) is how JavaScript sees your HTML. Use <code>document.querySelector()</code> to find elements by CSS selector, then modify them with properties like <code>textContent</code>, <code>style</code>, and <code>classList</code>.</p>
|
||||
<a href="#js-dom/0" class="topic-link">Practice DOM Methods</a>
|
||||
</div>
|
||||
<div class="topic-code">
|
||||
<div class="code-block">
|
||||
<pre><code>const title = document.querySelector('h1');
|
||||
title.textContent = "New Title";
|
||||
title.style.color = "coral";
|
||||
title.classList.add("active");</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="topic-row">
|
||||
<div class="topic-text">
|
||||
<h2>Event Handling</h2>
|
||||
<p>Events let your code respond to user actions. Use <code>addEventListener()</code> to run a function when something happens—a click, a keystroke, or an input change. The callback receives an event object with details about what happened.</p>
|
||||
<a href="#js-events/0" class="topic-link">Handle Events</a>
|
||||
</div>
|
||||
<div class="topic-code">
|
||||
<div class="code-block">
|
||||
<pre><code>const btn = document.querySelector('.btn');
|
||||
|
||||
btn.addEventListener('click', () => {
|
||||
alert('Clicked!');
|
||||
});</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
|
||||
@@ -2310,7 +2373,7 @@ function showLandingPage() {
|
||||
*/
|
||||
function renderFooterLessonLinks() {
|
||||
const modules = lessonEngine.modules || [];
|
||||
const sectionGroups = { css: [], html: [] };
|
||||
const sectionGroups = { css: [], html: [], javascript: [] };
|
||||
|
||||
modules.forEach((module) => {
|
||||
if (module.excludeFromProgress) return;
|
||||
@@ -2347,7 +2410,7 @@ function renderFooterLessonLinks() {
|
||||
* Update progress indicators on landing page
|
||||
*/
|
||||
function updateLandingProgress() {
|
||||
["css", "html", "markdown"].forEach((sectionId) => { // tailwind temporarily disabled
|
||||
["css", "html", "markdown", "javascript"].forEach((sectionId) => { // tailwind temporarily disabled
|
||||
const progressEl = document.getElementById(`${sectionId}-progress`);
|
||||
if (progressEl) {
|
||||
const sectionModules = getModulesBySection(lessonEngine.modules, sectionId);
|
||||
|
||||
@@ -31,6 +31,9 @@ import filtersEN from "../../lessons/11-filters.json";
|
||||
import positioningEN from "../../lessons/12-positioning.json";
|
||||
import pseudoElementsEN from "../../lessons/13-pseudo-elements.json";
|
||||
import markdownBasicsEN from "../../lessons/40-markdown-basics.json";
|
||||
import jsVariablesEN from "../../lessons/50-js-variables.json";
|
||||
import jsDomEN from "../../lessons/51-js-dom.json";
|
||||
import jsEventsEN from "../../lessons/52-js-events.json";
|
||||
import playgroundEN from "../../lessons/98-playground.json";
|
||||
import goodbyeEN from "../../lessons/99-goodbye.json";
|
||||
|
||||
@@ -165,6 +168,10 @@ const moduleStoreEN = [
|
||||
htmlTablesEN,
|
||||
// Markdown
|
||||
markdownBasicsEN,
|
||||
// JavaScript
|
||||
jsVariablesEN,
|
||||
jsDomEN,
|
||||
jsEventsEN,
|
||||
// Outro
|
||||
goodbyeEN,
|
||||
playgroundEN
|
||||
@@ -206,6 +213,10 @@ const moduleStoreDE = [
|
||||
htmlTablesDE,
|
||||
// Markdown
|
||||
markdownBasicsEN, // Using EN fallback until translated
|
||||
// JavaScript
|
||||
jsVariablesEN, // Using EN fallback until translated
|
||||
jsDomEN, // Using EN fallback until translated
|
||||
jsEventsEN, // Using EN fallback until translated
|
||||
// Outro
|
||||
goodbyeEN,
|
||||
playgroundEN
|
||||
@@ -247,6 +258,10 @@ const moduleStorePL = [
|
||||
htmlTablesPL,
|
||||
// Markdown
|
||||
markdownBasicsEN, // Using EN fallback until translated
|
||||
// JavaScript
|
||||
jsVariablesEN, // Using EN fallback until translated
|
||||
jsDomEN, // Using EN fallback until translated
|
||||
jsEventsEN, // Using EN fallback until translated
|
||||
// Outro
|
||||
goodbyeEN,
|
||||
playgroundEN
|
||||
@@ -288,6 +303,10 @@ const moduleStoreES = [
|
||||
htmlTablesES,
|
||||
// Markdown
|
||||
markdownBasicsEN, // Using EN fallback until translated
|
||||
// JavaScript
|
||||
jsVariablesEN, // Using EN fallback until translated
|
||||
jsDomEN, // Using EN fallback until translated
|
||||
jsEventsEN, // Using EN fallback until translated
|
||||
// Outro
|
||||
goodbyeEN,
|
||||
playgroundEN
|
||||
@@ -329,6 +348,10 @@ const moduleStoreAR = [
|
||||
htmlTablesAR,
|
||||
// Markdown
|
||||
markdownBasicsEN, // Using EN fallback until translated
|
||||
// JavaScript
|
||||
jsVariablesEN, // Using EN fallback until translated
|
||||
jsDomEN, // Using EN fallback until translated
|
||||
jsEventsEN, // Using EN fallback until translated
|
||||
// Outro
|
||||
goodbyeEN,
|
||||
playgroundEN
|
||||
@@ -370,6 +393,10 @@ const moduleStoreUK = [
|
||||
htmlTablesUK,
|
||||
// Markdown
|
||||
markdownBasicsEN, // Using EN fallback until translated
|
||||
// JavaScript
|
||||
jsVariablesEN, // Using EN fallback until translated
|
||||
jsDomEN, // Using EN fallback until translated
|
||||
jsEventsEN, // Using EN fallback until translated
|
||||
// Outro
|
||||
goodbyeEN,
|
||||
playgroundEN
|
||||
|
||||
@@ -31,6 +31,13 @@ export const sections = {
|
||||
description: "Lightweight markup language for formatting text",
|
||||
color: "#5b8dd9",
|
||||
order: 4
|
||||
},
|
||||
javascript: {
|
||||
id: "javascript",
|
||||
title: "JavaScript",
|
||||
description: "Interactive scripting for web pages",
|
||||
color: "#f0db4f",
|
||||
order: 5
|
||||
}
|
||||
};
|
||||
|
||||
@@ -65,6 +72,7 @@ export function getModuleSection(module) {
|
||||
if (mode === "html") return "html";
|
||||
if (mode === "tailwind") return "tailwind";
|
||||
if (mode === "markdown") return "markdown";
|
||||
if (mode === "javascript") return "javascript";
|
||||
return "css";
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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." };
|
||||
|
||||
@@ -8,6 +8,7 @@ import { history } from "@codemirror/commands";
|
||||
import { html } from "@codemirror/lang-html";
|
||||
import { css } from "@codemirror/lang-css";
|
||||
import { markdown } from "@codemirror/lang-markdown";
|
||||
import { javascript } from "@codemirror/lang-javascript";
|
||||
import { autocompletion } from "@codemirror/autocomplete";
|
||||
import { abbreviationTracker, expandAbbreviation } from "@emmetio/codemirror6-plugin";
|
||||
import { HighlightStyle, syntaxHighlighting } from "@codemirror/language";
|
||||
@@ -181,7 +182,8 @@ export class CodeEditor {
|
||||
const fullDoc = prefix + initialValue + suffix;
|
||||
|
||||
// Get language extension based on mode
|
||||
const langExtension = this.mode === "html" ? html() : this.mode === "markdown" ? markdown() : css();
|
||||
const langExtension =
|
||||
this.mode === "html" ? html() : this.mode === "javascript" ? javascript() : this.mode === "markdown" ? markdown() : css();
|
||||
|
||||
// Create read-only zones decorations
|
||||
const readOnlyMark = Decoration.mark({ class: "cm-readonly-zone" });
|
||||
|
||||
@@ -256,6 +256,30 @@ export class LessonEngine {
|
||||
${htmlWithClasses}
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
} else if (mode === "javascript") {
|
||||
// For JavaScript mode, user code runs as a script against previewHTML
|
||||
const { codePrefix, codeSuffix } = this.currentLesson;
|
||||
const fullScript = `${codePrefix || ""}${this.userCode || ""}${codeSuffix || ""}`;
|
||||
html = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>html, body { min-height: 100%; margin: 0; }</style>
|
||||
<style>${previewBaseCSS || ""}</style>
|
||||
<style>${sandboxCSS || ""}</style>
|
||||
</head>
|
||||
<body>
|
||||
${previewHTML || ""}
|
||||
<script>
|
||||
try {
|
||||
${fullScript}
|
||||
} catch (e) {
|
||||
document.body.innerHTML += '<pre style="color:red">' + e.message + '</pre>';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
} else if (mode === "markdown") {
|
||||
// For Markdown mode, parse user code to HTML
|
||||
@@ -382,6 +406,30 @@ export class LessonEngine {
|
||||
${htmlWithClasses}
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
} else if (mode === "javascript") {
|
||||
// For JavaScript mode, solution code runs as a script against previewHTML
|
||||
const { codePrefix, codeSuffix } = this.currentLesson;
|
||||
const fullScript = `${codePrefix || ""}${solutionCode}${codeSuffix || ""}`;
|
||||
html = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>html, body { min-height: 100%; margin: 0; }</style>
|
||||
<style>${previewBaseCSS || ""}</style>
|
||||
<style>${sandboxCSS || ""}</style>
|
||||
</head>
|
||||
<body>
|
||||
${previewHTML || ""}
|
||||
<script>
|
||||
try {
|
||||
${fullScript}
|
||||
} catch (e) {
|
||||
document.body.innerHTML += '<pre style="color:red">' + e.message + '</pre>';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
} else if (mode === "markdown") {
|
||||
// For Markdown mode, parse solution to HTML
|
||||
|
||||
@@ -77,6 +77,7 @@
|
||||
<a href="#html" class="nav-link" data-section="html">HTML</a>
|
||||
<!-- <a href="#tailwind" class="nav-link" data-section="tailwind">Tailwind</a> -->
|
||||
<a href="#markdown" class="nav-link" data-section="markdown">Markdown</a>
|
||||
<a href="#javascript" class="nav-link" data-section="javascript">JavaScript</a>
|
||||
<a href="#reference/css" class="nav-link nav-link-ref" data-section="reference">Reference</a>
|
||||
</nav>
|
||||
<button id="auth-trigger-header" class="btn btn-outline btn-sm" data-i18n="authLogin">Log In</button>
|
||||
@@ -178,6 +179,12 @@
|
||||
<p data-i18n="landingMarkdownDesc">Lightweight markup for formatting text</p>
|
||||
<span class="section-card-progress" id="markdown-progress"></span>
|
||||
</a>
|
||||
<a href="#javascript" class="section-card" data-section="javascript">
|
||||
<div class="section-card-icon" style="background: #f0db4f; color: #333">JS</div>
|
||||
<h3>JavaScript</h3>
|
||||
<p data-i18n="landingJsDesc">Interactive scripting for web pages</p>
|
||||
<span class="section-card-progress" id="javascript-progress"></span>
|
||||
</a>
|
||||
</div>
|
||||
<p class="device-notice" data-i18n-html="deviceNotice">
|
||||
<strong>Best on desktop or tablet (landscape).</strong> Mobile works, but larger screens make coding easier.
|
||||
@@ -194,13 +201,6 @@
|
||||
<h3 data-i18n="comingSoonAchievementsTitle">Achievements</h3>
|
||||
<p data-i18n="comingSoonAchievementsText">Earn badges as you master new skills. Track your learning milestones.</p>
|
||||
</article>
|
||||
<article class="coming-soon-card">
|
||||
<span class="coming-soon-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z"/></svg>
|
||||
</span>
|
||||
<h3 data-i18n="comingSoonJsTitle">JavaScript</h3>
|
||||
<p data-i18n="comingSoonJsText">Interactive JavaScript lessons with live code execution and DOM manipulation.</p>
|
||||
</article>
|
||||
<article class="coming-soon-card">
|
||||
<span class="coming-soon-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
|
||||
@@ -478,6 +478,7 @@
|
||||
<a href="#css" class="sidebar-nav-link" data-section="css">CSS</a>
|
||||
<a href="#html" class="sidebar-nav-link" data-section="html">HTML</a>
|
||||
<!-- <a href="#tailwind" class="sidebar-nav-link" data-section="tailwind">Tailwind</a> -->
|
||||
<a href="#javascript" class="sidebar-nav-link" data-section="javascript">JavaScript</a>
|
||||
<button id="auth-trigger-mobile" class="sidebar-nav-link sidebar-auth-link" data-i18n="authLogin">Log In</button>
|
||||
</nav>
|
||||
|
||||
|
||||
106
src/main.css
106
src/main.css
@@ -291,6 +291,14 @@ kbd {
|
||||
background: #5b8dd9;
|
||||
}
|
||||
|
||||
[data-section="javascript"] .logo h1 .code-text {
|
||||
color: #d4a017;
|
||||
}
|
||||
|
||||
[data-section="javascript"] .logo h1 .crispies-text {
|
||||
background: #d4a017;
|
||||
}
|
||||
|
||||
.help-toggle {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
@@ -1244,6 +1252,22 @@ nav.sidebar-section:not(.sidebar-nav-mobile) {
|
||||
animation: milestone-pop 0.5s ease-out;
|
||||
}
|
||||
|
||||
/* Sidebar section grouping headers */
|
||||
.sidebar-section-header {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--light-text);
|
||||
padding: 0.75rem 0.75rem 0.25rem;
|
||||
margin: 0.5rem 0 0;
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
|
||||
.sidebar-section-header:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* Module List in Sidebar */
|
||||
.module-list {
|
||||
/* No max-height - parent nav.sidebar-section handles overflow */
|
||||
@@ -3618,6 +3642,14 @@ input:checked + .toggle-slider::before {
|
||||
--section-color-rgb: 91, 141, 217;
|
||||
}
|
||||
|
||||
/* JavaScript Section - Gold */
|
||||
[data-section="javascript"] {
|
||||
--section-color: #d4a017;
|
||||
--section-color-light: #e0b840;
|
||||
--section-color-dark: #b08610;
|
||||
--section-color-rgb: 212, 160, 23;
|
||||
}
|
||||
|
||||
/* Apply section colors to nav links */
|
||||
.nav-link[data-section="css"] {
|
||||
color: #d95a8a;
|
||||
@@ -3635,6 +3667,10 @@ input:checked + .toggle-slider::before {
|
||||
color: #5b8dd9;
|
||||
}
|
||||
|
||||
.nav-link[data-section="javascript"] {
|
||||
color: #d4a017;
|
||||
}
|
||||
|
||||
.nav-link[data-section="css"]:hover,
|
||||
.nav-link[data-section="css"].active {
|
||||
background: rgba(217, 90, 138, 0.1);
|
||||
@@ -3659,6 +3695,12 @@ input:checked + .toggle-slider::before {
|
||||
color: #4070b8;
|
||||
}
|
||||
|
||||
.nav-link[data-section="javascript"]:hover,
|
||||
.nav-link[data-section="javascript"].active {
|
||||
background: rgba(212, 160, 23, 0.1);
|
||||
color: #b08610;
|
||||
}
|
||||
|
||||
/* Hint section colors */
|
||||
body[data-section="css"] .hint {
|
||||
background: rgba(217, 90, 138, 0.3);
|
||||
@@ -3696,6 +3738,15 @@ body[data-section="markdown"] .hint-progress {
|
||||
background: #5b8dd9;
|
||||
}
|
||||
|
||||
body[data-section="javascript"] .hint {
|
||||
background: rgba(212, 160, 23, 0.3);
|
||||
border-left-color: #e0b840;
|
||||
}
|
||||
|
||||
body[data-section="javascript"] .hint-progress {
|
||||
background: #d4a017;
|
||||
}
|
||||
|
||||
/* RTL hint border */
|
||||
[dir="rtl"] body[data-section="css"] .hint {
|
||||
border-right-color: #a98cd6;
|
||||
@@ -3713,6 +3764,10 @@ body[data-section="markdown"] .hint-progress {
|
||||
border-right-color: #7ba3e5;
|
||||
}
|
||||
|
||||
[dir="rtl"] body[data-section="javascript"] .hint {
|
||||
border-right-color: #e0b840;
|
||||
}
|
||||
|
||||
/* Reference nav link colors */
|
||||
.ref-nav-link[data-ref="css"],
|
||||
.ref-nav-link[data-ref="selectors"],
|
||||
@@ -3816,6 +3871,24 @@ body[data-section="markdown"] .cm-editor .cm-activeLine {
|
||||
background-color: rgba(91, 141, 217, 0.08) !important;
|
||||
}
|
||||
|
||||
body[data-section="javascript"] .cm-editor .cm-content {
|
||||
caret-color: #d4a017 !important;
|
||||
}
|
||||
|
||||
body[data-section="javascript"] .cm-editor .cm-cursor,
|
||||
body[data-section="javascript"] .cm-editor .cm-dropCursor {
|
||||
border-left-color: #d4a017 !important;
|
||||
}
|
||||
|
||||
body[data-section="javascript"] .cm-editor .cm-selectionBackground,
|
||||
body[data-section="javascript"] .cm-editor .cm-content ::selection {
|
||||
background-color: rgba(212, 160, 23, 0.25) !important;
|
||||
}
|
||||
|
||||
body[data-section="javascript"] .cm-editor .cm-activeLine {
|
||||
background-color: rgba(212, 160, 23, 0.08) !important;
|
||||
}
|
||||
|
||||
/* Module pill section colors */
|
||||
body[data-section="css"] .module-pill {
|
||||
background: rgba(217, 90, 138, 0.1);
|
||||
@@ -3853,6 +3926,15 @@ body[data-section="markdown"] .module-pill .level-indicator {
|
||||
color: #4070b8;
|
||||
}
|
||||
|
||||
body[data-section="javascript"] .module-pill {
|
||||
background: rgba(212, 160, 23, 0.1);
|
||||
color: #d4a017;
|
||||
}
|
||||
|
||||
body[data-section="javascript"] .module-pill .level-indicator {
|
||||
color: #b08610;
|
||||
}
|
||||
|
||||
/* Code block border section colors */
|
||||
body[data-section="css"] .code-block {
|
||||
border-color: rgba(217, 90, 138, 0.4);
|
||||
@@ -3870,6 +3952,10 @@ body[data-section="markdown"] .code-block {
|
||||
border-color: rgba(91, 141, 217, 0.4);
|
||||
}
|
||||
|
||||
body[data-section="javascript"] .code-block {
|
||||
border-color: rgba(212, 160, 23, 0.4);
|
||||
}
|
||||
|
||||
/* Section code block CodeMirror syntax highlighting overrides */
|
||||
body[data-section="css"] .code-block .cm-editor .cm-line {
|
||||
color: #c9c0e0;
|
||||
@@ -3887,6 +3973,10 @@ body[data-section="markdown"] .code-block .cm-editor .cm-line {
|
||||
color: #c0d0e8;
|
||||
}
|
||||
|
||||
body[data-section="javascript"] .code-block .cm-editor .cm-line {
|
||||
color: #e0d8b0;
|
||||
}
|
||||
|
||||
/* Task instruction bubble section colors */
|
||||
[data-section="css"] .task-instruction {
|
||||
background: rgba(217, 90, 138, 0.92);
|
||||
@@ -3904,6 +3994,10 @@ body[data-section="markdown"] .code-block .cm-editor .cm-line {
|
||||
background: rgba(91, 141, 217, 0.92);
|
||||
}
|
||||
|
||||
[data-section="javascript"] .task-instruction {
|
||||
background: rgba(212, 160, 23, 0.92);
|
||||
}
|
||||
|
||||
/* Section page progress bar colors */
|
||||
body[data-section="css"] .section-progress-bar .progress-fill {
|
||||
background: #d95a8a;
|
||||
@@ -3921,6 +4015,10 @@ body[data-section="markdown"] .section-progress-bar .progress-fill {
|
||||
background: #5b8dd9;
|
||||
}
|
||||
|
||||
body[data-section="javascript"] .section-progress-bar .progress-fill {
|
||||
background: #d4a017;
|
||||
}
|
||||
|
||||
/* Section page header colors */
|
||||
[data-section="css"] .section-hero h1 {
|
||||
color: #d95a8a;
|
||||
@@ -3938,6 +4036,10 @@ body[data-section="markdown"] .section-progress-bar .progress-fill {
|
||||
color: #5b8dd9;
|
||||
}
|
||||
|
||||
[data-section="javascript"] .section-hero h1 {
|
||||
color: #d4a017;
|
||||
}
|
||||
|
||||
/* Lesson title h2 section colors */
|
||||
body[data-section="css"] #lesson-title {
|
||||
color: #d95a8a;
|
||||
@@ -3955,6 +4057,10 @@ body[data-section="markdown"] #lesson-title {
|
||||
color: #5b8dd9;
|
||||
}
|
||||
|
||||
body[data-section="javascript"] #lesson-title {
|
||||
color: #d4a017;
|
||||
}
|
||||
|
||||
/* Section and Reference footer - override landing-footer styles */
|
||||
.section-footer.landing-footer,
|
||||
.reference-footer.landing-footer {
|
||||
|
||||
@@ -19,6 +19,10 @@ describe("Lessons Config Module", () => {
|
||||
expect(moduleIds).toContain("css-basic-selectors");
|
||||
expect(moduleIds).toContain("box-model");
|
||||
expect(moduleIds).toContain("flexbox");
|
||||
// JavaScript modules
|
||||
expect(moduleIds).toContain("js-variables");
|
||||
expect(moduleIds).toContain("js-dom");
|
||||
expect(moduleIds).toContain("js-events");
|
||||
});
|
||||
|
||||
test("should have mode set on each lesson", async () => {
|
||||
@@ -27,7 +31,7 @@ describe("Lessons Config Module", () => {
|
||||
modules.forEach((module) => {
|
||||
module.lessons.forEach((lesson) => {
|
||||
expect(lesson.mode).toBeDefined();
|
||||
expect(["html", "css", "tailwind", "markdown", "playground"]).toContain(lesson.mode);
|
||||
expect(["html", "css", "tailwind", "markdown", "javascript", "playground"]).toContain(lesson.mode);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -56,7 +56,8 @@ describe("Router", () => {
|
||||
test.each([
|
||||
["css", "css"],
|
||||
["html", "html"],
|
||||
["markdown", "markdown"]
|
||||
["markdown", "markdown"],
|
||||
["javascript", "javascript"]
|
||||
])("parseHash_SectionId_%s_ReturnsSectionRoute", (sectionId, expectedId) => {
|
||||
window.location.hash = `#${sectionId}`;
|
||||
const result = parseHash();
|
||||
@@ -220,6 +221,7 @@ describe("Router", () => {
|
||||
expect(ids).toContain("css");
|
||||
expect(ids).toContain("html");
|
||||
expect(ids).toContain("markdown");
|
||||
expect(ids).toContain("javascript");
|
||||
});
|
||||
|
||||
test("getSectionIds_MutatingCopy_DoesNotAffectOriginal", () => {
|
||||
|
||||
@@ -3,12 +3,13 @@ import { sections, getSection, getSectionList, getModuleSection, getModulesBySec
|
||||
|
||||
describe("Sections Config", () => {
|
||||
describe("sections constant", () => {
|
||||
test("sections_AllDefined_HasFourSections", () => {
|
||||
expect(Object.keys(sections)).toHaveLength(4);
|
||||
test("sections_AllDefined_HasFiveSections", () => {
|
||||
expect(Object.keys(sections)).toHaveLength(5);
|
||||
expect(sections).toHaveProperty("css");
|
||||
expect(sections).toHaveProperty("html");
|
||||
expect(sections).toHaveProperty("tailwind");
|
||||
expect(sections).toHaveProperty("markdown");
|
||||
expect(sections).toHaveProperty("javascript");
|
||||
});
|
||||
|
||||
test("sections_EachSection_HasRequiredFields", () => {
|
||||
@@ -27,7 +28,8 @@ describe("Sections Config", () => {
|
||||
["css", "CSS"],
|
||||
["html", "HTML"],
|
||||
["tailwind", "Tailwind CSS"],
|
||||
["markdown", "Markdown"]
|
||||
["markdown", "Markdown"],
|
||||
["javascript", "JavaScript"]
|
||||
])("getSection_%s_ReturnsCorrectSection", (id, expectedTitle) => {
|
||||
const section = getSection(id);
|
||||
expect(section).not.toBeNull();
|
||||
@@ -51,7 +53,7 @@ describe("Sections Config", () => {
|
||||
describe("getSectionList", () => {
|
||||
test("getSectionList_Default_ReturnsSortedByOrder", () => {
|
||||
const list = getSectionList();
|
||||
expect(list).toHaveLength(4);
|
||||
expect(list).toHaveLength(5);
|
||||
|
||||
// Verify sorted by order
|
||||
for (let i = 1; i < list.length; i++) {
|
||||
@@ -64,9 +66,9 @@ describe("Sections Config", () => {
|
||||
expect(list[0].id).toBe("css");
|
||||
});
|
||||
|
||||
test("getSectionList_Default_MarkdownIsLast", () => {
|
||||
test("getSectionList_Default_JavaScriptIsLast", () => {
|
||||
const list = getSectionList();
|
||||
expect(list[list.length - 1].id).toBe("markdown");
|
||||
expect(list[list.length - 1].id).toBe("javascript");
|
||||
});
|
||||
|
||||
test("getSectionList_Default_ContainsAllSections", () => {
|
||||
@@ -76,6 +78,7 @@ describe("Sections Config", () => {
|
||||
expect(ids).toContain("html");
|
||||
expect(ids).toContain("tailwind");
|
||||
expect(ids).toContain("markdown");
|
||||
expect(ids).toContain("javascript");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -89,7 +92,8 @@ describe("Sections Config", () => {
|
||||
["css", "css"],
|
||||
["html", "html"],
|
||||
["tailwind", "tailwind"],
|
||||
["markdown", "markdown"]
|
||||
["markdown", "markdown"],
|
||||
["javascript", "javascript"]
|
||||
])("getModuleSection_Mode%s_InfersCorrectSection", (mode, expectedSection) => {
|
||||
const module = { mode };
|
||||
expect(getModuleSection(module)).toBe(expectedSection);
|
||||
@@ -104,7 +108,7 @@ describe("Sections Config", () => {
|
||||
});
|
||||
|
||||
test("getModuleSection_UnknownMode_DefaultsToCss", () => {
|
||||
expect(getModuleSection({ mode: "javascript" })).toBe("css");
|
||||
expect(getModuleSection({ mode: "unknown-mode" })).toBe("css");
|
||||
});
|
||||
|
||||
test("getModuleSection_ExplicitSectionOverridesMode_UsesSection", () => {
|
||||
|
||||
@@ -226,6 +226,86 @@ describe("CSS Validator", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("JavaScript Validator", () => {
|
||||
describe("validateUserCode with mode: javascript", () => {
|
||||
it("should validate contains correctly for JavaScript", () => {
|
||||
const userCode = 'const name = "Alice";';
|
||||
const lesson = {
|
||||
mode: "javascript",
|
||||
validations: [{ type: "contains", value: "const", message: "Use const" }]
|
||||
};
|
||||
|
||||
const result = validateUserCode(userCode, lesson);
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.validCases).toBe(1);
|
||||
});
|
||||
|
||||
it("should validate regex correctly for JavaScript", () => {
|
||||
const userCode = 'const name = "Alice";';
|
||||
const lesson = {
|
||||
mode: "javascript",
|
||||
validations: [{ type: "regex", value: 'const\\s+name\\s*=', message: "Declare name" }]
|
||||
};
|
||||
|
||||
const result = validateUserCode(userCode, lesson);
|
||||
expect(result.isValid).toBe(true);
|
||||
});
|
||||
|
||||
it("should validate not_contains correctly for JavaScript", () => {
|
||||
const userCode = 'const name = "Alice";';
|
||||
const lesson = {
|
||||
mode: "javascript",
|
||||
validations: [{ type: "not_contains", value: "var", message: "Do not use var" }]
|
||||
};
|
||||
|
||||
const result = validateUserCode(userCode, lesson);
|
||||
expect(result.isValid).toBe(true);
|
||||
|
||||
const failCode = 'var name = "Alice";';
|
||||
const failResult = validateUserCode(failCode, lesson);
|
||||
expect(failResult.isValid).toBe(false);
|
||||
expect(failResult.message).toBe("Do not use var");
|
||||
});
|
||||
|
||||
it("should return invalid for missing code", () => {
|
||||
const userCode = "";
|
||||
const lesson = {
|
||||
mode: "javascript",
|
||||
validations: [{ type: "contains", value: "const", message: "Use const" }]
|
||||
};
|
||||
|
||||
const result = validateUserCode(userCode, lesson);
|
||||
expect(result.isValid).toBe(false);
|
||||
});
|
||||
|
||||
it("should pass with no validations", () => {
|
||||
const userCode = 'const x = 1;';
|
||||
const lesson = { mode: "javascript" };
|
||||
|
||||
const result = validateUserCode(userCode, lesson);
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.message).toContain("No validations specified");
|
||||
});
|
||||
|
||||
it("should handle multiple validations with early return on failure", () => {
|
||||
const userCode = 'const name = "Alice";';
|
||||
const lesson = {
|
||||
mode: "javascript",
|
||||
validations: [
|
||||
{ type: "contains", value: "const", message: "Use const" },
|
||||
{ type: "contains", value: "let", message: "Use let" },
|
||||
{ type: "contains", value: "name", message: "Declare name" }
|
||||
]
|
||||
};
|
||||
|
||||
const result = validateUserCode(userCode, lesson);
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.message).toBe("Use let");
|
||||
expect(result.validCases).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("HTML Validator", () => {
|
||||
describe("validateUserCode with mode: html", () => {
|
||||
it("should validate element_exists correctly", () => {
|
||||
|
||||
Reference in New Issue
Block a user