diff --git a/lessons/50-js-variables.json b/lessons/50-js-variables.json new file mode 100644 index 0000000..533b8f4 --- /dev/null +++ b/lessons/50-js-variables.json @@ -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 const 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 name with the value \"Ada\"", + "previewHTML": "
Waiting...
", + "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 const to declare the variable" + }, + { + "type": "regex", + "value": "const\\s+name\\s*=", + "message": "Name your constant name" + }, + { + "type": "regex", + "value": "[\"']Ada[\"']", + "message": "Set the value to \"Ada\"" + } + ] + }, + { + "id": "js-let", + "title": "Let Variables", + "description": "Use let to declare a variable that can be reassigned later. This is useful when you need to update a value.", + "task": "Declare a variable score with let and set it to 0", + "previewHTML": "Waiting...
", + "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 let to declare the variable" + }, + { + "type": "regex", + "value": "let\\s+score\\s*=\\s*0", + "message": "Set score to 0" + } + ] + }, + { + "id": "js-template", + "title": "Template Literals", + "description": "Template literals use backticks ` and ${} to embed expressions inside strings. They make building dynamic text much easier than string concatenation.", + "task": "Create a const msg using a template literal: `Hi, ${name}!`", + "previewHTML": "Waiting...
", + "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 ${} to embed the variable inside the template" + }, + { + "type": "regex", + "value": "`[^`]*\\$\\{\\s*name\\s*\\}[^`]*`", + "message": "Embed name inside a template literal with backticks" + }, + { + "type": "regex", + "value": "const\\s+msg\\s*=", + "message": "Assign the result to a constant named msg" + } + ] + } + ] +} diff --git a/lessons/51-js-dom.json b/lessons/51-js-dom.json new file mode 100644 index 0000000..00c1a33 --- /dev/null +++ b/lessons/51-js-dom.json @@ -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 document.querySelector() to find an element by its CSS selector. It returns the first matching element.", + "task": "Select the element with id box and store it in a const el", + "previewHTML": "", + "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 document.querySelector() to select the element" + }, + { + "type": "regex", + "value": "querySelector\\s*\\([\"']#box[\"']\\)", + "message": "Pass \"#box\" as the selector" + }, + { + "type": "regex", + "value": "const\\s+el\\s*=", + "message": "Store the result in a constant named el" + } + ] + }, + { + "id": "js-text", + "title": "Change Text", + "description": "The textContent property lets you read or change the text inside an element. Setting it replaces all the element's text.", + "task": "Set the textContent of el to \"Hello!\"", + "previewHTML": "Old text
", + "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 textContent property" + }, + { + "type": "regex", + "value": "el\\.textContent\\s*=\\s*[\"']Hello![\"']", + "message": "Set el.textContent to \"Hello!\"" + } + ] + }, + { + "id": "js-style", + "title": "Change Style", + "description": "Access an element's inline styles through the style property. CSS properties use camelCase in JavaScript, so background-color becomes backgroundColor.", + "task": "Set el.style.backgroundColor to \"gold\"", + "previewHTML": "", + "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 el.style.backgroundColor" + }, + { + "type": "regex", + "value": "\\.style\\.backgroundColor\\s*=\\s*[\"']gold[\"']", + "message": "Set the background color to \"gold\"" + } + ] + } + ] +} diff --git a/lessons/52-js-events.json b/lessons/52-js-events.json new file mode 100644 index 0000000..d164569 --- /dev/null +++ b/lessons/52-js-events.json @@ -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 addEventListener(\"click\", ...) 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 click event listener to btn that sets out.textContent to \"Clicked!\"", + "previewHTML": "\nWaiting...
", + "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 addEventListener to listen for events" + }, + { + "type": "regex", + "value": "addEventListener\\s*\\(\\s*[\"']click[\"']", + "message": "Listen for the \"click\" event" + }, + { + "type": "regex", + "value": "textContent\\s*=\\s*[\"']Clicked![\"']", + "message": "Set out.textContent to \"Clicked!\" inside the handler" + } + ] + }, + { + "id": "js-toggle", + "title": "Toggle a Class", + "description": "Use classList.toggle() 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 box.classList.toggle(\"on\")", + "previewHTML": "\n", + "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 classList.toggle() to toggle the class" + }, + { + "type": "regex", + "value": "classList\\.toggle\\s*\\(\\s*[\"']on[\"']\\s*\\)", + "message": "Toggle the class \"on\"" + }, + { + "type": "contains", + "value": "addEventListener", + "message": "Use addEventListener to listen for clicks" + } + ] + }, + { + "id": "js-counter", + "title": "Simple Counter", + "description": "Combine variables and event listeners to build interactive features. Use let for a value that changes, and update the display each time the button is clicked.", + "task": "In the click handler, increment count by 1 and set out.textContent to count", + "previewHTML": "\n0
", + "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 addEventListener to listen for clicks" + }, + { + "type": "regex", + "value": "count\\s*\\+\\+|count\\s*\\+=\\s*1|count\\s*=\\s*count\\s*\\+\\s*1", + "message": "Increment count by 1 (use count++)" + }, + { + "type": "regex", + "value": "out\\.textContent\\s*=\\s*count", + "message": "Update the display with out.textContent = count" + } + ] + } + ] +} diff --git a/package-lock.json b/package-lock.json index 510a9c5..df8012b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 9386db1..a644707 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/schemas/code-crispies-module-schema.json b/schemas/code-crispies-module-schema.json index 2f2fc5b..8df3094 100644 --- a/schemas/code-crispies-module-schema.json +++ b/schemas/code-crispies-module-schema.json @@ -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": { diff --git a/src/app.js b/src/app.js index e069860..76411ff 100644 --- a/src/app.js +++ b/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: "\n\n", label: "HTML & CSS", @@ -1493,6 +1498,64 @@ This is \`inline code\`. + `, + javascript: ` +JavaScript is the programming language of the web. It makes pages interactive—responding to clicks, updating content, and manipulating the DOM (Document Object Model). Every modern website uses JavaScript to create dynamic user experiences.
+Start with the fundamentals: declaring variables with const and let, selecting elements with querySelector, changing content and styles, and responding to user events like clicks.
Store values using const (cannot be reassigned) and let (can be updated). Use template literals with backticks to build dynamic strings with embedded expressions.
const name = "Ada";
+let score = 0;
+const msg = \`Hi, \${name}!\`;
+ Use document.querySelector() to find elements, textContent to change text, and style to modify CSS properties directly from JavaScript.
+ Practice DOM +
+const el = document.querySelector("#box");
+el.textContent = "Hello!";
+el.style.backgroundColor = "gold";
+ Use addEventListener to respond to user interactions. Handle clicks, toggle classes, and build interactive features like counters and toggles.
+ Practice events +
+btn.addEventListener("click", () => {
+ count++;
+ out.textContent = count;
+});
+