Compare commits
1 Commits
main
...
feat/impl-
| Author | SHA1 | Date | |
|---|---|---|---|
| 26b9b99937 |
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>"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
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 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.</p>
|
||||
<p>Start with the fundamentals: declaring variables with <code>const</code> and <code>let</code>, selecting elements with <code>querySelector</code>, changing content and styles, and responding to user events like clicks.</p>
|
||||
</div>
|
||||
|
||||
<div class="topic-row">
|
||||
<div class="topic-text">
|
||||
<h2>Variables</h2>
|
||||
<p>Store values using <code>const</code> (cannot be reassigned) and <code>let</code> (can be updated). Use template literals with backticks to build dynamic strings with embedded expressions.</p>
|
||||
<p>
|
||||
<a href="#js-variables/0" class="topic-link">Practice variables</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="topic-code">
|
||||
<div class="code-block">
|
||||
<pre><code>const name = "Ada";
|
||||
let score = 0;
|
||||
const msg = \`Hi, \${name}!\`;</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="topic-row">
|
||||
<div class="topic-text">
|
||||
<h2>DOM Manipulation</h2>
|
||||
<p>Use <code>document.querySelector()</code> to find elements, <code>textContent</code> to change text, and <code>style</code> to modify CSS properties directly from JavaScript.</p>
|
||||
<p>
|
||||
<a href="#js-dom/0" class="topic-link">Practice DOM</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="topic-code">
|
||||
<div class="code-block">
|
||||
<pre><code>const el = document.querySelector("#box");
|
||||
el.textContent = "Hello!";
|
||||
el.style.backgroundColor = "gold";</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="topic-row">
|
||||
<div class="topic-text">
|
||||
<h2>Events</h2>
|
||||
<p>Use <code>addEventListener</code> to respond to user interactions. Handle clicks, toggle classes, and build interactive features like counters and toggles.</p>
|
||||
<p>
|
||||
<a href="#js-events/0" class="topic-link">Practice events</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="topic-code">
|
||||
<div class="code-block">
|
||||
<pre><code>btn.addEventListener("click", () => {
|
||||
count++;
|
||||
out.textContent = count;
|
||||
});</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: [], markdown: [], 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
|
||||
@@ -385,6 +412,58 @@ const moduleStores = {
|
||||
uk: moduleStoreUK
|
||||
};
|
||||
|
||||
/**
|
||||
* Category labels for sidebar section headers.
|
||||
* Maps module IDs to their visual grouping label.
|
||||
*/
|
||||
const moduleCategories = {
|
||||
// CSS Basics
|
||||
"css-basic-selectors": "CSS Basics",
|
||||
colors: "CSS Basics",
|
||||
gradients: "CSS Basics",
|
||||
typography: "CSS Basics",
|
||||
"box-model": "CSS Basics",
|
||||
// CSS Layout
|
||||
flexbox: "CSS Layout",
|
||||
grid: "CSS Layout",
|
||||
positioning: "CSS Layout",
|
||||
"units-variables": "CSS Layout",
|
||||
responsive: "CSS Layout",
|
||||
// CSS Polish
|
||||
"transitions-animations": "CSS Polish",
|
||||
filters: "CSS Polish",
|
||||
"pseudo-elements": "CSS Polish",
|
||||
// HTML Structure
|
||||
"html-elements": "HTML Structure",
|
||||
"html-semantic": "HTML Structure",
|
||||
"html-figure": "HTML Structure",
|
||||
"html-svg": "HTML Structure",
|
||||
// HTML Interactive
|
||||
"html-details-summary": "HTML Interactive",
|
||||
"html-dialog": "HTML Interactive",
|
||||
"html-progress-meter": "HTML Interactive",
|
||||
"html-forms-basic": "HTML Interactive",
|
||||
"html-forms-validation": "HTML Interactive",
|
||||
"html-forms-fieldset": "HTML Interactive",
|
||||
"html-datalist": "HTML Interactive",
|
||||
"html-tables": "HTML Interactive",
|
||||
// Markdown
|
||||
"markdown-basics": "Markdown",
|
||||
// JavaScript
|
||||
"js-variables": "JavaScript",
|
||||
"js-dom": "JavaScript",
|
||||
"js-events": "JavaScript"
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the sidebar category label for a module
|
||||
* @param {string} moduleId - The module ID
|
||||
* @returns {string|null} The category label, or null for uncategorized modules (welcome, outro)
|
||||
*/
|
||||
export function getModuleCategory(moduleId) {
|
||||
return moduleCategories[moduleId] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all available modules for a given language
|
||||
* @param {string} language - Language code ('en', 'de', 'pl', 'es', 'ar', 'uk')
|
||||
|
||||
@@ -31,6 +31,13 @@ export const sections = {
|
||||
description: "Lightweight markup language for formatting text",
|
||||
color: "#5b8dd9",
|
||||
order: 4
|
||||
},
|
||||
javascript: {
|
||||
id: "javascript",
|
||||
title: "JavaScript",
|
||||
description: "Variables, DOM manipulation, and event handling",
|
||||
color: "#f0c040",
|
||||
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 { getModuleCategory } from "../config/lessons.js";
|
||||
|
||||
/**
|
||||
* Compute lesson difficulty based on lesson structure
|
||||
@@ -72,8 +73,21 @@ export function renderModuleList(container, modules, onSelectModule, onSelectLes
|
||||
}
|
||||
}
|
||||
|
||||
// Track current category for section headers
|
||||
let currentCategory = null;
|
||||
|
||||
// Create list items for each module
|
||||
modules.forEach((module) => {
|
||||
// Insert section header when category changes
|
||||
const category = getModuleCategory(module.id);
|
||||
if (category && category !== currentCategory) {
|
||||
currentCategory = category;
|
||||
const header = document.createElement("h3");
|
||||
header.className = "module-section-header";
|
||||
header.textContent = category;
|
||||
header.setAttribute("aria-hidden", "true");
|
||||
container.appendChild(header);
|
||||
}
|
||||
// Create module container
|
||||
// Use native <details>/<summary> for expand/collapse
|
||||
const moduleContainer = document.createElement("details");
|
||||
|
||||
@@ -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,7 @@ 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 === "markdown" ? markdown() : this.mode === "javascript" ? javascript() : css();
|
||||
|
||||
// Create read-only zones decorations
|
||||
const readOnlyMark = Decoration.mark({ class: "cm-readonly-zone" });
|
||||
|
||||
@@ -288,6 +288,30 @@ export class LessonEngine {
|
||||
${renderedHtml}
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
} else if (mode === "javascript") {
|
||||
// For JavaScript mode, inject user code as a script
|
||||
const { codePrefix, codeSuffix } = this.currentLesson;
|
||||
const fullScript = `${codePrefix || ""}${this.userCode || ""}${codeSuffix || ""}`;
|
||||
iframeDoc.write(`
|
||||
<!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) {
|
||||
console.error("Script error:", e);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
} else {
|
||||
// Original CSS mode
|
||||
@@ -414,6 +438,30 @@ export class LessonEngine {
|
||||
${renderedHtml}
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
} else if (mode === "javascript") {
|
||||
// For JavaScript mode, inject solution code as a script
|
||||
const { codePrefix, codeSuffix } = this.currentLesson;
|
||||
const fullScript = `${codePrefix || ""}${solutionCode}${codeSuffix || ""}`;
|
||||
iframeDoc.write(`
|
||||
<!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) {
|
||||
console.error("Script error:", e);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
} else {
|
||||
// CSS mode - wrap solution with prefix/suffix
|
||||
|
||||
@@ -76,6 +76,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">JS</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>
|
||||
@@ -177,6 +178,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: #f0c040">JS</div>
|
||||
<h3>JavaScript</h3>
|
||||
<p data-i18n="landingJsDesc">Variables, DOM manipulation, and event handling</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.
|
||||
@@ -477,6 +484,8 @@
|
||||
<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="#markdown" class="sidebar-nav-link" data-section="markdown">Markdown</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>
|
||||
|
||||
|
||||
101
src/main.css
101
src/main.css
@@ -291,6 +291,14 @@ kbd {
|
||||
background: #5b8dd9;
|
||||
}
|
||||
|
||||
[data-section="javascript"] .logo h1 .code-text {
|
||||
color: #d4a020;
|
||||
}
|
||||
|
||||
[data-section="javascript"] .logo h1 .crispies-text {
|
||||
background: #d4a020;
|
||||
}
|
||||
|
||||
.help-toggle {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
@@ -1249,6 +1257,17 @@ nav.sidebar-section:not(.sidebar-nav-mobile) {
|
||||
/* No max-height - parent nav.sidebar-section handles overflow */
|
||||
}
|
||||
|
||||
.module-section-header {
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
color: #888;
|
||||
letter-spacing: 0.08em;
|
||||
padding: 0.75rem var(--spacing-sm) 0.25rem;
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.module-container {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
@@ -3618,6 +3637,14 @@ input:checked + .toggle-slider::before {
|
||||
--section-color-rgb: 91, 141, 217;
|
||||
}
|
||||
|
||||
/* JavaScript Section - Gold */
|
||||
[data-section="javascript"] {
|
||||
--section-color: #d4a020;
|
||||
--section-color-light: #e0b840;
|
||||
--section-color-dark: #b08818;
|
||||
--section-color-rgb: 212, 160, 32;
|
||||
}
|
||||
|
||||
/* Apply section colors to nav links */
|
||||
.nav-link[data-section="css"] {
|
||||
color: #9163b8;
|
||||
@@ -3635,6 +3662,10 @@ input:checked + .toggle-slider::before {
|
||||
color: #5b8dd9;
|
||||
}
|
||||
|
||||
.nav-link[data-section="javascript"] {
|
||||
color: #d4a020;
|
||||
}
|
||||
|
||||
.nav-link[data-section="css"]:hover,
|
||||
.nav-link[data-section="css"].active {
|
||||
background: rgba(145, 99, 184, 0.1);
|
||||
@@ -3659,6 +3690,12 @@ input:checked + .toggle-slider::before {
|
||||
color: #4070b8;
|
||||
}
|
||||
|
||||
.nav-link[data-section="javascript"]:hover,
|
||||
.nav-link[data-section="javascript"].active {
|
||||
background: rgba(212, 160, 32, 0.1);
|
||||
color: #b08818;
|
||||
}
|
||||
|
||||
/* Hint section colors */
|
||||
body[data-section="css"] .hint {
|
||||
background: rgba(145, 99, 184, 0.3);
|
||||
@@ -3696,6 +3733,15 @@ body[data-section="markdown"] .hint-progress {
|
||||
background: #5b8dd9;
|
||||
}
|
||||
|
||||
body[data-section="javascript"] .hint {
|
||||
background: rgba(212, 160, 32, 0.3);
|
||||
border-left-color: #e0b840;
|
||||
}
|
||||
|
||||
body[data-section="javascript"] .hint-progress {
|
||||
background: #d4a020;
|
||||
}
|
||||
|
||||
/* RTL hint border */
|
||||
[dir="rtl"] body[data-section="css"] .hint {
|
||||
border-right-color: #a98cd6;
|
||||
@@ -3713,6 +3759,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 +3866,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: #d4a020 !important;
|
||||
}
|
||||
|
||||
body[data-section="javascript"] .cm-editor .cm-cursor,
|
||||
body[data-section="javascript"] .cm-editor .cm-dropCursor {
|
||||
border-left-color: #d4a020 !important;
|
||||
}
|
||||
|
||||
body[data-section="javascript"] .cm-editor .cm-selectionBackground,
|
||||
body[data-section="javascript"] .cm-editor .cm-content ::selection {
|
||||
background-color: rgba(212, 160, 32, 0.25) !important;
|
||||
}
|
||||
|
||||
body[data-section="javascript"] .cm-editor .cm-activeLine {
|
||||
background-color: rgba(212, 160, 32, 0.08) !important;
|
||||
}
|
||||
|
||||
/* Module pill section colors */
|
||||
body[data-section="css"] .module-pill {
|
||||
background: rgba(145, 99, 184, 0.1);
|
||||
@@ -3853,6 +3921,15 @@ body[data-section="markdown"] .module-pill .level-indicator {
|
||||
color: #4070b8;
|
||||
}
|
||||
|
||||
body[data-section="javascript"] .module-pill {
|
||||
background: rgba(212, 160, 32, 0.1);
|
||||
color: #d4a020;
|
||||
}
|
||||
|
||||
body[data-section="javascript"] .module-pill .level-indicator {
|
||||
color: #b08818;
|
||||
}
|
||||
|
||||
/* Code block border section colors */
|
||||
body[data-section="css"] .code-block {
|
||||
border-color: rgba(145, 99, 184, 0.4);
|
||||
@@ -3870,6 +3947,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, 32, 0.4);
|
||||
}
|
||||
|
||||
/* Section code block CodeMirror syntax highlighting overrides */
|
||||
body[data-section="css"] .code-block .cm-editor .cm-line {
|
||||
color: #c9c0e0;
|
||||
@@ -3887,6 +3968,10 @@ body[data-section="markdown"] .code-block .cm-editor .cm-line {
|
||||
color: #c0d0e8;
|
||||
}
|
||||
|
||||
body[data-section="javascript"] .code-block .cm-editor .cm-line {
|
||||
color: #e8dcc0;
|
||||
}
|
||||
|
||||
/* Task instruction bubble section colors */
|
||||
[data-section="css"] .task-instruction {
|
||||
background: rgba(145, 99, 184, 0.92);
|
||||
@@ -3904,6 +3989,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, 32, 0.92);
|
||||
}
|
||||
|
||||
/* Section page progress bar colors */
|
||||
body[data-section="css"] .section-progress-bar .progress-fill {
|
||||
background: #9163b8;
|
||||
@@ -3921,6 +4010,10 @@ body[data-section="markdown"] .section-progress-bar .progress-fill {
|
||||
background: #5b8dd9;
|
||||
}
|
||||
|
||||
body[data-section="javascript"] .section-progress-bar .progress-fill {
|
||||
background: #d4a020;
|
||||
}
|
||||
|
||||
/* Section page header colors */
|
||||
[data-section="css"] .section-hero h1 {
|
||||
color: #9163b8;
|
||||
@@ -3938,6 +4031,10 @@ body[data-section="markdown"] .section-progress-bar .progress-fill {
|
||||
color: #5b8dd9;
|
||||
}
|
||||
|
||||
[data-section="javascript"] .section-hero h1 {
|
||||
color: #d4a020;
|
||||
}
|
||||
|
||||
/* Lesson title h2 section colors */
|
||||
body[data-section="css"] #lesson-title {
|
||||
color: #9163b8;
|
||||
@@ -3955,6 +4052,10 @@ body[data-section="markdown"] #lesson-title {
|
||||
color: #5b8dd9;
|
||||
}
|
||||
|
||||
body[data-section="javascript"] #lesson-title {
|
||||
color: #d4a020;
|
||||
}
|
||||
|
||||
/* Section and Reference footer - override landing-footer styles */
|
||||
.section-footer.landing-footer,
|
||||
.reference-footer.landing-footer {
|
||||
|
||||
@@ -27,7 +27,35 @@ 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("JavaScript modules", () => {
|
||||
test("should include JavaScript modules", async () => {
|
||||
const modules = await loadModules();
|
||||
const moduleIds = modules.map((m) => m.id);
|
||||
|
||||
expect(moduleIds).toContain("js-variables");
|
||||
expect(moduleIds).toContain("js-dom");
|
||||
expect(moduleIds).toContain("js-events");
|
||||
});
|
||||
|
||||
test("JavaScript modules should have correct mode and structure", async () => {
|
||||
const modules = await loadModules();
|
||||
const jsModules = modules.filter((m) => m.mode === "javascript");
|
||||
|
||||
expect(jsModules.length).toBe(3);
|
||||
|
||||
jsModules.forEach((module) => {
|
||||
expect(module.lessons.length).toBeGreaterThanOrEqual(3);
|
||||
module.lessons.forEach((lesson) => {
|
||||
expect(lesson.mode).toBe("javascript");
|
||||
expect(lesson.validations.length).toBeGreaterThan(0);
|
||||
expect(lesson.task).toBeTruthy();
|
||||
expect(lesson.solution).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -240,4 +240,67 @@ describe("Renderer Module", () => {
|
||||
expect(computeLessonDifficulty({ codePrefix: null })).toBe("medium");
|
||||
});
|
||||
});
|
||||
|
||||
describe("renderModuleList section headers", () => {
|
||||
const noop = () => {};
|
||||
|
||||
test("inserts section header elements between different category groups", () => {
|
||||
const container = document.getElementById("module-list");
|
||||
const modules = [
|
||||
{ id: "css-basic-selectors", title: "CSS Selectors", lessons: [{ title: "L1" }] },
|
||||
{ id: "colors", title: "Colors", lessons: [{ title: "L1" }] },
|
||||
{ id: "flexbox", title: "Flexbox", lessons: [{ title: "L1" }] },
|
||||
{ id: "html-elements", title: "HTML Elements", lessons: [{ title: "L1" }] }
|
||||
];
|
||||
|
||||
renderModuleList(container, modules, noop, noop);
|
||||
|
||||
const headers = container.querySelectorAll(".module-section-header");
|
||||
expect(headers.length).toBe(3); // CSS Basics, CSS Layout, HTML Structure
|
||||
});
|
||||
|
||||
test("section headers display correct category text", () => {
|
||||
const container = document.getElementById("module-list");
|
||||
const modules = [
|
||||
{ id: "css-basic-selectors", title: "CSS Selectors", lessons: [{ title: "L1" }] },
|
||||
{ id: "flexbox", title: "Flexbox", lessons: [{ title: "L1" }] }
|
||||
];
|
||||
|
||||
renderModuleList(container, modules, noop, noop);
|
||||
|
||||
const headers = container.querySelectorAll(".module-section-header");
|
||||
expect(headers[0].textContent).toBe("CSS Basics");
|
||||
expect(headers[1].textContent).toBe("CSS Layout");
|
||||
});
|
||||
|
||||
test("no section header is inserted between modules in the same category", () => {
|
||||
const container = document.getElementById("module-list");
|
||||
const modules = [
|
||||
{ id: "css-basic-selectors", title: "CSS Selectors", lessons: [{ title: "L1" }] },
|
||||
{ id: "colors", title: "Colors", lessons: [{ title: "L1" }] },
|
||||
{ id: "typography", title: "Typography", lessons: [{ title: "L1" }] }
|
||||
];
|
||||
|
||||
renderModuleList(container, modules, noop, noop);
|
||||
|
||||
const headers = container.querySelectorAll(".module-section-header");
|
||||
expect(headers.length).toBe(1);
|
||||
expect(headers[0].textContent).toBe("CSS Basics");
|
||||
});
|
||||
|
||||
test("Welcome and Outro modules have no section headers", () => {
|
||||
const container = document.getElementById("module-list");
|
||||
const modules = [
|
||||
{ id: "welcome", title: "Welcome", lessons: [{ title: "L1" }] },
|
||||
{ id: "css-basic-selectors", title: "CSS Selectors", lessons: [{ title: "L1" }] },
|
||||
{ id: "playground", title: "Playground", lessons: [{ title: "L1" }] }
|
||||
];
|
||||
|
||||
renderModuleList(container, modules, noop, noop);
|
||||
|
||||
const headers = container.querySelectorAll(".module-section-header");
|
||||
expect(headers.length).toBe(1);
|
||||
expect(headers[0].textContent).toBe("CSS Basics");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -226,6 +226,69 @@ describe("CSS Validator", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("JavaScript Validator", () => {
|
||||
describe("validateUserCode with mode: javascript", () => {
|
||||
it("should pass contains validation for correct code", () => {
|
||||
const userCode = 'const name = "Ada";';
|
||||
const lesson = {
|
||||
mode: "javascript",
|
||||
validations: [{ type: "contains", value: "const", message: "Use const" }]
|
||||
};
|
||||
|
||||
const result = validateUserCode(userCode, lesson);
|
||||
expect(result.isValid).toBe(true);
|
||||
});
|
||||
|
||||
it("should fail contains validation for missing code", () => {
|
||||
const userCode = 'var name = "Ada";';
|
||||
const lesson = {
|
||||
mode: "javascript",
|
||||
validations: [{ type: "contains", value: "const", message: "Use const keyword" }]
|
||||
};
|
||||
|
||||
const result = validateUserCode(userCode, lesson);
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.message).toBe("Use const keyword");
|
||||
});
|
||||
|
||||
it("should pass regex validation", () => {
|
||||
const userCode = 'const name = "Ada";';
|
||||
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 handle not_contains validation", () => {
|
||||
const userCode = "let score = 0;";
|
||||
const lesson = {
|
||||
mode: "javascript",
|
||||
validations: [{ type: "not_contains", value: "var", message: "Don't use var" }]
|
||||
};
|
||||
|
||||
const result = validateUserCode(userCode, lesson);
|
||||
expect(result.isValid).toBe(true);
|
||||
|
||||
const failLesson = {
|
||||
mode: "javascript",
|
||||
validations: [{ type: "not_contains", value: "let", message: "Don't use let" }]
|
||||
};
|
||||
|
||||
const failResult = validateUserCode(userCode, failLesson);
|
||||
expect(failResult.isValid).toBe(false);
|
||||
});
|
||||
|
||||
it("should pass with no validations", () => {
|
||||
const result = validateUserCode("const x = 1;", { mode: "javascript" });
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.message).toContain("No validations specified");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("HTML Validator", () => {
|
||||
describe("validateUserCode with mode: html", () => {
|
||||
it("should validate element_exists correctly", () => {
|
||||
|
||||
Reference in New Issue
Block a user