From 9249f122a4ceb90d9fb1718325af49e83b07b9d7 Mon Sep 17 00:00:00 2001 From: Michael Czechowski Date: Wed, 14 May 2025 00:51:10 +0200 Subject: [PATCH] feat: restructure lesson files and update success indicators --- lessons/{basics.json => 00-basics.json} | 0 lessons/01-box-model.json | 99 ++++++++++++++++++ lessons/02-selectors.json | 102 +++++++++++++++++++ lessons/03-colors.json | 120 ++++++++++++++++++++++ lessons/04-typography.json | 90 ++++++++++++++++ lessons/05-units-variables.json | 97 ++++++++++++++++++ lessons/06-transitions-animations.json | 130 ++++++++++++++++++++++++ lessons/07-layouts.json | 99 ++++++++++++++++++ lessons/08-responsive.json | 116 +++++++++++++++++++++ public/bar_1680535.png | Bin 0 -> 3809 bytes src/app.js | 8 +- src/config/lessons.js | 25 ++++- src/helpers/renderer.js | 14 +-- src/index.html | 3 +- src/main.css | 128 +++++++++++++++++------ tests/unit/lessons.test.js | 2 +- vite.config.js | 8 +- 17 files changed, 990 insertions(+), 51 deletions(-) rename lessons/{basics.json => 00-basics.json} (100%) create mode 100644 lessons/01-box-model.json create mode 100644 lessons/02-selectors.json create mode 100644 lessons/03-colors.json create mode 100644 lessons/04-typography.json create mode 100644 lessons/05-units-variables.json create mode 100644 lessons/06-transitions-animations.json create mode 100644 lessons/07-layouts.json create mode 100644 lessons/08-responsive.json create mode 100644 public/bar_1680535.png diff --git a/lessons/basics.json b/lessons/00-basics.json similarity index 100% rename from lessons/basics.json rename to lessons/00-basics.json diff --git a/lessons/01-box-model.json b/lessons/01-box-model.json new file mode 100644 index 0000000..9818eda --- /dev/null +++ b/lessons/01-box-model.json @@ -0,0 +1,99 @@ +{ + "id": "box-model", + "title": "CSS Box Model Essentials", + "description": "Understand how CSS calculates sizes and spacing around elements using margin, border, padding, and content areas.", + "difficulty": "beginner", + "lessons": [ + { + "id": "box-model-1", + "title": "Content, Padding, Border, Margin", + "description": "Learn the four layers of the CSS box model and how each affects element dimensions.", + "task": "Create a div with class 'box' and add 1.25rem padding, a 0.125rem solid #333 border, and 1rem margin.", + "previewHTML": "
Box Model Demo
", + "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .box { background-color: #f0f0f0; }", + "sandboxCSS": "", + "codePrefix": "/* Style the box element */\n.box {", + "initialCode": "", + "codeSuffix": "}", + "previewContainer": "preview-area", + "validations": [ + { "type": "contains", "value": "padding", "message": "Use the 'padding' property", "options": { "caseSensitive": false } }, + { "type": "property_value", "value": { "property": "padding", "expected": "1.25rem" }, "message": "Padding should be 1.25rem" }, + { "type": "contains", "value": "border", "message": "Use the 'border' property", "options": { "caseSensitive": false } }, + { + "type": "regex", + "value": "border:\\s*0.125rem\\s+solid\\s+#333", + "message": "Border should be '0.125rem solid #333'", + "options": { "caseSensitive": false } + }, + { "type": "contains", "value": "margin", "message": "Use the 'margin' property", "options": { "caseSensitive": false } }, + { "type": "property_value", "value": { "property": "margin", "expected": "1rem" }, "message": "Margin should be 1rem" } + ] + }, + { + "id": "box-model-2", + "title": "Box-Sizing Property", + "description": "Discover how box-sizing changes the box model calculation for width and height.", + "task": "Set box-sizing: border-box; on the .box element and note how its width includes padding and border.", + "previewHTML": "
Border-box Demo
", + "previewBaseCSS": "body { font-family: sans-serif; } .box { width: 200px; padding: 1rem; border: 0.125rem solid #333; background: #eef; }", + "sandboxCSS": "", + "codePrefix": "/* Apply box-sizing */\n.box {", + "initialCode": "", + "codeSuffix": "}", + "previewContainer": "preview-area", + "validations": [ + { "type": "contains", "value": "box-sizing", "message": "Include the 'box-sizing' property", "options": { "caseSensitive": false } }, + { + "type": "property_value", + "value": { "property": "box-sizing", "expected": "border-box" }, + "message": "Set box-sizing to 'border-box'" + } + ] + }, + { + "id": "box-model-3", + "title": "Margin Collapse", + "description": "Understand how vertical margins can collapse between adjacent elements.", + "task": "Create two stacked divs with class 'box' and 1rem top margin on each. Observe margin collapse.", + "previewHTML": "
First
Second
", + "previewBaseCSS": "body { font-family: sans-serif; } .box { padding: 1rem; background: #ddd; margin-top: 1rem; }", + "sandboxCSS": "", + "codePrefix": "", + "initialCode": "", + "codeSuffix": "", + "previewContainer": "preview-area", + "validations": [ + { "type": "contains", "value": "margin-top", "message": "Use 'margin-top' on .box", "options": { "caseSensitive": false } }, + { "type": "property_value", "value": { "property": "margin-top", "expected": "1rem" }, "message": "Top margin should be 1rem" } + ] + }, + { + "id": "box-model-4", + "title": "Using Shorthand Properties", + "description": "Learn to write concise padding and margin using shorthand notation.", + "task": "Refactor separate margin and padding properties into a single shorthand declaration on .box.", + "previewHTML": "
Shorthand Demo
", + "previewBaseCSS": "body { font-family: sans-serif; } .box { background: #fef; }", + "sandboxCSS": "", + "codePrefix": "/* Refactor to shorthand */\n.box {", + "initialCode": "margin: 1rem 0 2rem 0; padding: 0.5rem 1rem 0.5rem 1rem;", + "codeSuffix": "}", + "previewContainer": "preview-area", + "validations": [ + { + "type": "regex", + "value": "margin:\\s*1rem\\s+0\\s+2rem\\s+0", + "message": "Use shorthand for margin: 1rem 0 2rem 0", + "options": { "caseSensitive": false } + }, + { + "type": "regex", + "value": "padding:\\s*0.5rem\\s+1rem\\s+0.5rem\\s+1rem", + "message": "Use shorthand for padding: 0.5rem 1rem 0.5rem 1rem", + "options": { "caseSensitive": false } + } + ] + } + ] +} diff --git a/lessons/02-selectors.json b/lessons/02-selectors.json new file mode 100644 index 0000000..f7137b7 --- /dev/null +++ b/lessons/02-selectors.json @@ -0,0 +1,102 @@ +{ + "id": "selectors", + "title": "CSS Selectors Deep Dive", + "description": "Master the art of targeting HTML elements using various CSS selectors, from basics to specificity rules.", + "difficulty": "beginner", + "lessons": [ + { + "id": "selectors-1", + "title": "Element Selectors", + "description": "Learn to target HTML elements by their tag name and apply styling.", + "task": "Use the element selector to make all <p> tags have a dark gray color.", + "previewHTML": "

This paragraph should be dark gray.

This one too!

", + "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; }", + "sandboxCSS": "", + "codePrefix": "/* Style all paragraphs */\n", + "initialCode": "", + "codeSuffix": "", + "previewContainer": "preview-area", + "validations": [ + { "type": "contains", "value": "p {", "message": "Use the element selector 'p'", "options": { "caseSensitive": false } }, + { "type": "contains", "value": "color", "message": "Include the 'color' property", "options": { "caseSensitive": false } }, + { + "type": "property_value", + "value": { "property": "color", "expected": "darkgray" }, + "message": "Set color to 'darkgray'", + "options": { "exact": false } + } + ] + }, + { + "id": "selectors-2", + "title": "Class Selectors", + "description": "Use class selectors to style elements sharing the same class name.", + "task": "Apply a blueviolet text color to any element with the class 'title'.", + "previewHTML": "

Hello World

Another Heading

", + "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; }", + "sandboxCSS": "", + "codePrefix": "/* Style elements with class 'title' */\n", + "initialCode": "", + "codeSuffix": "", + "previewContainer": "preview-area", + "validations": [ + { "type": "contains", "value": ".title", "message": "Use the '.title' class selector", "options": { "caseSensitive": false } }, + { + "type": "property_value", + "value": { "property": "color", "expected": "blueviolet" }, + "message": "Set color to 'blueviolet'", + "options": { "exact": false } + } + ] + }, + { + "id": "selectors-3", + "title": "ID Selectors", + "description": "Target a unique element by its ID to apply specific styling.", + "task": "Make the element with id=\"description\" have orangered text.", + "previewHTML": "
This is the description text.
This is another div.
", + "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; }", + "sandboxCSS": "", + "codePrefix": "/* Style the element with ID 'description' */\n", + "initialCode": "", + "codeSuffix": "", + "previewContainer": "preview-area", + "validations": [ + { + "type": "contains", + "value": "#description", + "message": "Use the '#description' ID selector", + "options": { "caseSensitive": false } + }, + { + "type": "property_value", + "value": { "property": "color", "expected": "orangered" }, + "message": "Set color to 'orangered'", + "options": { "exact": false } + } + ] + }, + { + "id": "selectors-4", + "title": "Combined Selectors & Specificity", + "description": "Discover how combining selectors controls which rules take precedence.", + "task": "Use a class and element selector together (e.g., div.note) to give a yellow background to the note box.", + "previewHTML": "
Important note!
Regular div
", + "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; }", + "sandboxCSS": "", + "codePrefix": "/* Style div with class 'note' */\n", + "initialCode": "", + "codeSuffix": "", + "previewContainer": "preview-area", + "validations": [ + { "type": "contains", "value": "div.note", "message": "Use the 'div.note' combined selector", "options": { "caseSensitive": false } }, + { + "type": "property_value", + "value": { "property": "background-color", "expected": "yellow" }, + "message": "Set background-color to 'yellow'", + "options": { "exact": false } + } + ] + } + ] +} diff --git a/lessons/03-colors.json b/lessons/03-colors.json new file mode 100644 index 0000000..1148a2f --- /dev/null +++ b/lessons/03-colors.json @@ -0,0 +1,120 @@ +{ + "id": "colors-backgrounds", + "title": "Colors & Backgrounds", + "description": "Learn how to apply and manipulate colors, backgrounds, and graphical fills using CSS properties.", + "difficulty": "beginner", + "lessons": [ + { + "id": "colors-1", + "title": "Setting Background Colors", + "description": "Use the background-color property to fill elements with solid colors.", + "task": "Apply a light cyan background (#e0f7fa) to the element with class 'colorbox'.", + "previewHTML": "
Background Demo
", + "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .colorbox { padding: 1rem; }", + "sandboxCSS": "", + "codePrefix": "/* Set a background color */\n.colorbox {", + "initialCode": "", + "codeSuffix": "}", + "previewContainer": "preview-area", + "validations": [ + { "type": "contains", "value": ".colorbox", "message": "Select '.colorbox'", "options": { "caseSensitive": false } }, + { + "type": "contains", + "value": "background-color", + "message": "Use 'background-color' property", + "options": { "caseSensitive": false } + }, + { + "type": "property_value", + "value": { "property": "background-color", "expected": "#e0f7fa" }, + "message": "Set background-color to '#e0f7fa'", + "options": { "exact": true } + } + ] + }, + { + "id": "colors-2", + "title": "Text Color and Contrast", + "description": "Apply the color property to control text readability against backgrounds.", + "task": "Set the text color of '.colorbox' to deep blue (#01579b). Ensure good contrast.", + "previewHTML": "
Color & Contrast
", + "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .colorbox { padding: 1rem; background: #e0f7fa; }", + "sandboxCSS": "", + "codePrefix": "/* Set text color */\n.colorbox {", + "initialCode": "", + "codeSuffix": "}", + "previewContainer": "preview-area", + "validations": [ + { "type": "contains", "value": ".colorbox", "message": "Select '.colorbox'", "options": { "caseSensitive": false } }, + { "type": "contains", "value": "color", "message": "Use the 'color' property", "options": { "caseSensitive": false } }, + { + "type": "property_value", + "value": { "property": "color", "expected": "#01579b" }, + "message": "Set color to '#01579b'", + "options": { "exact": true } + } + ] + }, + { + "id": "colors-3", + "title": "CSS Gradients", + "description": "Learn to create smooth transitions between colors using linear and radial gradients.", + "task": "Apply a linear gradient background from #ff9a9e to #fad0c4 on an element with class 'gradient-box'.", + "previewHTML": "
Gradient Demo
", + "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .gradient-box { padding: 1rem; color: white; text-align: center; }", + "sandboxCSS": "", + "codePrefix": "/* Set a linear gradient background */\n.gradient-box {", + "initialCode": "", + "codeSuffix": "}", + "previewContainer": "preview-area", + "validations": [ + { "type": "contains", "value": ".gradient-box", "message": "Select '.gradient-box'", "options": { "caseSensitive": false } }, + { + "type": "contains", + "value": "background-image", + "message": "Use 'background-image' property", + "options": { "caseSensitive": false } + }, + { + "type": "regex", + "value": "linear-gradient\\(.*#ff9a9e.*,.*#fad0c4.*\\)", + "message": "Use linear-gradient from #ff9a9e to #fad0c4", + "options": { "caseSensitive": false } + } + ] + }, + { + "id": "colors-4", + "title": "Background Images & Repeat", + "description": "Add images as backgrounds and control repetition and positioning.", + "task": "Set a background image on '.bg-img' using a placeholder URL, center it, and prevent tiling.", + "previewHTML": "
Image Background
", + "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .bg-img { height: 150px; display: flex; align-items: center; justify-content: center; color: white; }", + "sandboxCSS": "", + "codePrefix": "/* Set background image */\n.bg-img {", + "initialCode": "background-image: url('https://via.placeholder.com/300'); background-position: center; background-repeat: no-repeat;", + "codeSuffix": "}", + "previewContainer": "preview-area", + "validations": [ + { + "type": "contains", + "value": "background-image", + "message": "Use 'background-image' property", + "options": { "caseSensitive": false } + }, + { + "type": "contains", + "value": "background-position: center", + "message": "Center the background image", + "options": { "caseSensitive": false } + }, + { + "type": "contains", + "value": "background-repeat: no-repeat", + "message": "Prevent image tiling", + "options": { "caseSensitive": false } + } + ] + } + ] +} diff --git a/lessons/04-typography.json b/lessons/04-typography.json new file mode 100644 index 0000000..a21ff87 --- /dev/null +++ b/lessons/04-typography.json @@ -0,0 +1,90 @@ +{ + "id": "typography-fonts", + "title": "Typography & Fonts", + "description": "Learn how to control text appearance through font selection, sizing, spacing, and decorative effects.", + "difficulty": "beginner", + "lessons": [ + { + "id": "typography-1", + "title": "Font Family & Fallbacks", + "description": "Specify custom fonts and reliable fallback stacks for consistent typography across devices.", + "task": "Set the font-family of the '.text' element to 'Georgia, serif' with serif fallback.", + "previewHTML": "

This text shows the chosen font family.

", + "previewBaseCSS": "body { padding: 1rem; }", + "sandboxCSS": "", + "codePrefix": "/* Set font family */\n.text {", + "initialCode": "", + "codeSuffix": "}", + "previewContainer": "preview-area", + "validations": [ + { "type": "contains", "value": "font-family", "message": "Use the 'font-family' property", "options": { "caseSensitive": false } }, + { + "type": "regex", + "value": "Georgia, serif", + "message": "Include 'Georgia, serif' in the font stack", + "options": { "caseSensitive": false } + } + ] + }, + { + "id": "typography-2", + "title": "Font Size & Line Height", + "description": "Control text scale and readability by adjusting size and line heights.", + "task": "Set the heading '.heading' to 1.5rem font-size and 1.5 line-height.", + "previewHTML": "

Readable Heading

", + "previewBaseCSS": "body { padding: 1rem; }", + "sandboxCSS": "", + "codePrefix": "/* Set size and line height */\n.heading {", + "initialCode": "", + "codeSuffix": "}", + "previewContainer": "preview-area", + "validations": [ + { "type": "contains", "value": "font-size", "message": "Use 'font-size' property", "options": { "caseSensitive": false } }, + { "type": "property_value", "value": { "property": "font-size", "expected": "1.5rem" }, "message": "Set font-size to '1.5rem'" }, + { "type": "contains", "value": "line-height", "message": "Use 'line-height' property", "options": { "caseSensitive": false } }, + { "type": "property_value", "value": { "property": "line-height", "expected": "1.5" }, "message": "Set line-height to '1.5'" } + ] + }, + { + "id": "typography-3", + "title": "Font Weight & Style", + "description": "Apply weight and style variations like bold, light, italic to emphasize text.", + "task": "Make the paragraph '.emphasis' italic and bold using font-style and font-weight.", + "previewHTML": "

This text should stand out.

", + "previewBaseCSS": "body { padding: 1rem; }", + "sandboxCSS": "", + "codePrefix": "/* Emphasize text */\n.emphasis {", + "initialCode": "", + "codeSuffix": "}", + "previewContainer": "preview-area", + "validations": [ + { "type": "contains", "value": "font-style", "message": "Use 'font-style' property", "options": { "caseSensitive": false } }, + { "type": "property_value", "value": { "property": "font-style", "expected": "italic" }, "message": "Set font-style to 'italic'" }, + { "type": "contains", "value": "font-weight", "message": "Use 'font-weight' property", "options": { "caseSensitive": false } }, + { "type": "property_value", "value": { "property": "font-weight", "expected": "bold" }, "message": "Set font-weight to 'bold'" } + ] + }, + { + "id": "typography-4", + "title": "Text Decoration & Shadow", + "description": "Add decorative underlines, overlines, line-throughs and subtle shadows to text.", + "task": "Apply an underline with text-decoration and a light shadow using text-shadow on '.fancy'.", + "previewHTML": "

Fancy text effect!

", + "previewBaseCSS": "body { padding: 1rem; } .fancy { font-size: 1.25rem; }", + "sandboxCSS": "", + "codePrefix": "/* Decorate text */\n.fancy {", + "initialCode": "", + "codeSuffix": "}", + "previewContainer": "preview-area", + "validations": [ + { + "type": "contains", + "value": "text-decoration", + "message": "Use 'text-decoration' property", + "options": { "caseSensitive": false } + }, + { "type": "contains", "value": "text-shadow", "message": "Use 'text-shadow' property", "options": { "caseSensitive": false } } + ] + } + ] +} diff --git a/lessons/05-units-variables.json b/lessons/05-units-variables.json new file mode 100644 index 0000000..279736c --- /dev/null +++ b/lessons/05-units-variables.json @@ -0,0 +1,97 @@ +{ + "id": "units-variables", + "title": "CSS Units & Variables", + "description": "Understand the variety of CSS measurement units and how to define and use custom properties for maintainable styles.", + "difficulty": "beginner", + "lessons": [ + { + "id": "units-1", + "title": "Absolute vs. Relative Units", + "description": "Learn the difference between px, rem, em, %, and vw/vh for flexible, responsive layouts.", + "task": "Set the width of '.unit-box' to 80% and max-width to 37.5rem.", + "previewHTML": "
Resize me!
", + "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .unit-box { background: #f5f5f5; padding: 1rem; }", + "sandboxCSS": "", + "codePrefix": "/* Set flexible sizing */\n.unit-box {", + "initialCode": "", + "codeSuffix": "}", + "previewContainer": "preview-area", + "validations": [ + { "type": "contains", "value": "width", "message": "Use 'width' property", "options": { "caseSensitive": false } }, + { "type": "property_value", "value": { "property": "width", "expected": "80%" }, "message": "Set width to '80%'" }, + { "type": "contains", "value": "max-width", "message": "Use 'max-width' property", "options": { "caseSensitive": false } }, + { "type": "property_value", "value": { "property": "max-width", "expected": "37.5rem" }, "message": "Set max-width to '37.5rem'" } + ] + }, + { + "id": "units-2", + "title": "CSS Custom Properties", + "description": "Define and reuse variables (--custom properties) to centralize your theme values.", + "task": "Create a --main-color variable in :root with #6200ee and apply it as the border color on '.var-box'.", + "previewHTML": "
Variable Box
", + "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .var-box { padding: 1rem; border: 0.125rem solid #ddd; }", + "sandboxCSS": "", + "codePrefix": "/* Define and use a CSS variable */\n:root {", + "initialCode": "", + "codeSuffix": "}\n.var-box { }", + "previewContainer": "preview-area", + "validations": [ + { "type": "contains", "value": "--main-color", "message": "Define '--main-color' in :root", "options": { "caseSensitive": false } }, + { "type": "contains", "value": "var(--main-color)", "message": "Use var(--main-color)", "options": { "caseSensitive": false } }, + { + "type": "property_value", + "value": { "property": "border", "expected": "var(--main-color)" }, + "message": "Apply variable to border color", + "options": { "exact": false } + } + ] + }, + { + "id": "units-3", + "title": "Unit Calculations (calc)", + "description": "Use the calc() function to combine different units in one expression.", + "task": "Set the width of '.calc-box' to calc(100% - 2rem) and min-height to calc(10vh + 1rem).", + "previewHTML": "
Calc Demo
", + "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .calc-box { background: #e8f5e9; padding: 1rem; }", + "sandboxCSS": "", + "codePrefix": "/* Use calc for dynamic sizing */\n.calc-box {", + "initialCode": "", + "codeSuffix": "}", + "previewContainer": "preview-area", + "validations": [ + { "type": "contains", "value": "calc", "message": "Use 'calc()' function", "options": { "caseSensitive": false } }, + { + "type": "regex", + "value": "width:\\s*calc\\(100% - 2rem\\)", + "message": "Width should be calc(100% - 2rem)", + "options": { "caseSensitive": false } + }, + { + "type": "regex", + "value": "min-height:\\s*calc\\(10vh \\+ 1rem\\)", + "message": "Min-height should be calc(10vh + 1rem)", + "options": { "caseSensitive": false } + } + ] + }, + { + "id": "units-4", + "title": "Viewport & Responsive Units", + "description": "Control layouts relative to viewport size with vw, vh, and vmin/vmax units.", + "task": "Give '.viewport-box' a width of 50vw and height of 20vh.", + "previewHTML": "
Viewport Box
", + "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .viewport-box { background: #ffe0b2; }", + "sandboxCSS": "", + "codePrefix": "/* Use viewport units */\n.viewport-box {", + "initialCode": "", + "codeSuffix": "}", + "previewContainer": "preview-area", + "validations": [ + { "type": "contains", "value": "vw", "message": "Use 'vw' unit", "options": { "caseSensitive": false } }, + { "type": "contains", "value": "vh", "message": "Use 'vh' unit", "options": { "caseSensitive": false } }, + { "type": "property_value", "value": { "property": "width", "expected": "50vw" }, "message": "Set width to '50vw'" }, + { "type": "property_value", "value": { "property": "height", "expected": "20vh" }, "message": "Set height to '20vh'" } + ] + } + ] +} diff --git a/lessons/06-transitions-animations.json b/lessons/06-transitions-animations.json new file mode 100644 index 0000000..a37b718 --- /dev/null +++ b/lessons/06-transitions-animations.json @@ -0,0 +1,130 @@ +{ + "id": "transitions-animations", + "title": "CSS Transitions & Animations", + "description": "Bring interactivity to your UI by smoothly transitioning properties and creating keyframe-driven animations.", + "difficulty": "beginner", + "lessons": [ + { + "id": "transitions-1", + "title": "Simple Transitions", + "description": "Learn how to apply transition to properties for smooth changes on state changes.", + "task": "Add a hover transition on a button so its background-color fades over 0.3s.", + "previewHTML": "", + "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .btn { background: #6200ee; color: white; padding: 0.5rem 1rem; border: none; } .btn:hover { background: #3700b3; }", + "sandboxCSS": "", + "codePrefix": "/* Add transition */\n.btn {", + "initialCode": "", + "codeSuffix": "}", + "previewContainer": "preview-area", + "validations": [ + { "type": "contains", "value": "transition", "message": "Use the 'transition' property", "options": { "caseSensitive": false } }, + { + "type": "regex", + "value": "transition:\\s*background-color\\s*0\\.3s", + "message": "Transition background-color over 0.3s", + "options": { "caseSensitive": false } + } + ] + }, + { + "id": "transitions-2", + "title": "Transition Timing Functions", + "description": "Explore easing functions like ease, linear, ease-in, ease-out to control animation pacing.", + "task": "Modify the button to use 'ease-in-out' timing for its transition.", + "previewHTML": "", + "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .btn { background: #6200ee; color: white; padding: 0.5rem 1rem; border: none; transition: background-color 0.3s; } .btn:hover { background: #03dac6; }", + "sandboxCSS": "", + "codePrefix": "/* Set timing function */\n.btn {", + "initialCode": "", + "codeSuffix": "}", + "previewContainer": "preview-area", + "validations": [ + { + "type": "contains", + "value": "transition-timing-function", + "message": "Use 'transition-timing-function'", + "options": { "caseSensitive": false } + }, + { + "type": "property_value", + "value": { "property": "transition-timing-function", "expected": "ease-in-out" }, + "message": "Set timing to 'ease-in-out'" + } + ] + }, + { + "id": "transitions-3", + "title": "Keyframe Animations Basics", + "description": "Create named animations using @keyframes and apply them via the animation shorthand.", + "task": "Define a keyframe named 'bounce' that moves an element up 20px at 50% and apply it to '.ball' over 1s infinite.", + "previewHTML": "
", + "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .ball { width: 50px; height: 50px; background: #ff0266; border-radius: 50%; margin: 2rem auto; }", + "sandboxCSS": "", + "codePrefix": "/* Define keyframes and apply animation */\n@keyframes bounce {", + "initialCode": "", + "codeSuffix": "}\n.ball { }", + "previewContainer": "preview-area", + "validations": [ + { "type": "contains", "value": "@keyframes bounce", "message": "Define '@keyframes bounce'", "options": { "caseSensitive": false } }, + { + "type": "regex", + "value": "50%.*transform: translateY\\(-20px\\)", + "message": "At 50%, move up 20px", + "options": { "caseSensitive": false } + }, + { "type": "contains", "value": "animation", "message": "Use 'animation' property on .ball", "options": { "caseSensitive": false } }, + { + "type": "regex", + "value": "animation:.*bounce.*1s.*infinite", + "message": "Apply 'bounce 1s infinite'", + "options": { "caseSensitive": false } + } + ] + }, + { + "id": "transitions-4", + "title": "Animation Properties Deep Dive", + "description": "Fine-tune animations with delay, iteration-count, direction, and fill-mode.", + "task": "Animate '.box' using a 'fade' keyframe over 2s, delay 1s, run twice, and remain visible after.", + "previewHTML": "
Fade Demo
", + "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .box { width: 100px; height: 100px; background: #4caf50; margin: 2rem auto; }", + "sandboxCSS": "", + "codePrefix": "/* Define fade and set properties */\n@keyframes fade { from { opacity: 0; } to { opacity: 1; } }\n.box {", + "initialCode": "", + "codeSuffix": "}", + "previewContainer": "preview-area", + "validations": [ + { + "type": "contains", + "value": "animation-delay", + "message": "Use 'animation-delay' property", + "options": { "caseSensitive": false } + }, + { + "type": "contains", + "value": "animation-iteration-count", + "message": "Use 'animation-iteration-count' property", + "options": { "caseSensitive": false } + }, + { + "type": "contains", + "value": "animation-fill-mode", + "message": "Use 'animation-fill-mode' property", + "options": { "caseSensitive": false } + }, + { "type": "property_value", "value": { "property": "animation-duration", "expected": "2s" }, "message": "Duration should be 2s" }, + { "type": "property_value", "value": { "property": "animation-delay", "expected": "1s" }, "message": "Delay should be 1s" }, + { + "type": "property_value", + "value": { "property": "animation-iteration-count", "expected": "2" }, + "message": "Iteration count should be 2" + }, + { + "type": "property_value", + "value": { "property": "animation-fill-mode", "expected": "forwards" }, + "message": "Fill mode should be forwards" + } + ] + } + ] +} diff --git a/lessons/07-layouts.json b/lessons/07-layouts.json new file mode 100644 index 0000000..adee7d5 --- /dev/null +++ b/lessons/07-layouts.json @@ -0,0 +1,99 @@ +{ + "id": "layouts", + "title": "Advanced Layouts: Flexbox & Grid", + "description": "Master modern CSS layout techniques with Flexbox and Grid for responsive, powerful designs.", + "difficulty": "intermediate", + "lessons": [ + { + "id": "layouts-1", + "title": "Flexbox Fundamentals", + "description": "Learn the core properties of Flexbox to align, distribute space, and order items in a container.", + "task": "Set .flex-container to display: flex, and center its children both horizontally and vertically.", + "previewHTML": "
1
2
3
", + "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .flex-container > div { background: #eceff1; margin: 0.5rem; padding: 1rem; }", + "sandboxCSS": "", + "codePrefix": "/* Enable and center Flexbox */\n.flex-container {", + "initialCode": "", + "codeSuffix": "}", + "previewContainer": "preview-area", + "validations": [ + { "type": "contains", "value": "display", "message": "Use 'display: flex'", "options": { "caseSensitive": false } }, + { "type": "contains", "value": "justify-content", "message": "Use 'justify-content: center'", "options": { "caseSensitive": false } }, + { "type": "contains", "value": "align-items", "message": "Use 'align-items: center'", "options": { "caseSensitive": false } } + ] + }, + { + "id": "layouts-2", + "title": "Flexbox Advanced Features", + "description": "Control wrapping, ordering, and flexible growth/shrink of items in a flex container.", + "task": "Allow items to wrap and set .flex-item to flex: 1 1 100px.", + "previewHTML": "
A
B
C
", + "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .flex-item { background: #c5cae9; margin: 0.5rem; padding: 1rem; }", + "sandboxCSS": "", + "codePrefix": "/* Enable wrap and flexible items */\n.flex-container {", + "initialCode": "", + "codeSuffix": "}\n.flex-item { }", + "previewContainer": "preview-area", + "validations": [ + { "type": "contains", "value": "flex-wrap: wrap", "message": "Use 'flex-wrap: wrap'", "options": { "caseSensitive": false } }, + { + "type": "regex", + "value": ".flex-item.*flex:\\s*1\\s+1\\s+100px", + "message": "Set flex: 1 1 100px on items", + "options": { "caseSensitive": false } + } + ] + }, + { + "id": "layouts-3", + "title": "Grid Layout Basics", + "description": "Define grid containers, set rows and columns, and place items in a structured grid.", + "task": "Set .grid-container to display: grid with three equal columns and a 1rem gap.", + "previewHTML": "
1
2
3
4
", + "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .grid-container > div { background: #ffe082; padding: 1rem; }", + "sandboxCSS": "", + "codePrefix": "/* Define Grid */\n.grid-container {", + "initialCode": "", + "codeSuffix": "}", + "previewContainer": "preview-area", + "validations": [ + { "type": "contains", "value": "display: grid", "message": "Use 'display: grid'", "options": { "caseSensitive": false } }, + { + "type": "contains", + "value": "grid-template-columns", + "message": "Define 'grid-template-columns'", + "options": { "caseSensitive": false } + }, + { + "type": "regex", + "value": "grid-template-columns:\\s*repeat\\(3,\\s*1fr\\)\\s*", + "message": "Create three equal columns", + "options": { "caseSensitive": false } + }, + { "type": "contains", "value": "gap", "message": "Use 'gap' property", "options": { "caseSensitive": false } } + ] + }, + { + "id": "layouts-4", + "title": "Grid Item Placement", + "description": "Control the span and position of grid items with grid-column and grid-row.", + "task": "Span the first grid item across 2 columns using grid-column: 1 / span 2.", + "previewHTML": "
Featured
2
3
4
", + "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .grid-container > div { background: #c8e6c9; padding: 1rem; }", + "sandboxCSS": "", + "codePrefix": "/* Span item */\n.item1 {", + "initialCode": "", + "codeSuffix": "}", + "previewContainer": "preview-area", + "validations": [ + { "type": "contains", "value": "grid-column", "message": "Use 'grid-column' property", "options": { "caseSensitive": false } }, + { + "type": "property_value", + "value": { "property": "grid-column", "expected": "1 / span 2" }, + "message": "Span across 2 columns", + "options": { "caseSensitive": false } + } + ] + } + ] +} diff --git a/lessons/08-responsive.json b/lessons/08-responsive.json new file mode 100644 index 0000000..b45f7aa --- /dev/null +++ b/lessons/08-responsive.json @@ -0,0 +1,116 @@ +{ + "id": "responsive-design", + "title": "Responsive Design & Media Queries", + "description": "Make your layouts adapt to different screen sizes using media queries and fluid design techniques.", + "difficulty": "intermediate", + "lessons": [ + { + "id": "responsive-1", + "title": "Introduction to Media Queries", + "description": "Understand the syntax and use cases for CSS media queries to apply styles conditionally based on viewport characteristics.", + "task": "Write a media query that applies when the viewport is at most 600px wide and changes the background of '.responsive-box' to lightcoral.", + "previewHTML": "
Resize the window
", + "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .responsive-box { padding: 1rem; background: lightblue; }", + "sandboxCSS": "", + "codePrefix": "/* Add your media query below */\n", + "initialCode": "", + "codeSuffix": "", + "previewContainer": "preview-area", + "validations": [ + { + "type": "regex", + "value": "@media\\s*\\(max-width:\\s*600px\\)", + "message": "Use a media query for max-width: 600px", + "options": { "caseSensitive": false } + }, + { + "type": "contains", + "value": ".responsive-box", + "message": "Target '.responsive-box' inside the media query", + "options": { "caseSensitive": false } + }, + { "type": "contains", "value": "background", "message": "Change the 'background' property", "options": { "caseSensitive": false } }, + { + "type": "property_value", + "value": { "property": "background", "expected": "lightcoral" }, + "message": "Set background to 'lightcoral'", + "options": { "exact": false } + } + ] + }, + { + "id": "responsive-2", + "title": "Fluid Typography", + "description": "Use relative units like vw to make font sizes scale with the viewport width.", + "task": "Set the font-size of '.fluid-text' to 5vw so it scales as the viewport changes.", + "previewHTML": "

Fluid Typography

", + "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; }", + "sandboxCSS": "", + "codePrefix": "/* Apply fluid font sizing */\n.fluid-text {", + "initialCode": "", + "codeSuffix": "}", + "previewContainer": "preview-area", + "validations": [ + { "type": "contains", "value": "font-size", "message": "Use 'font-size' property", "options": { "caseSensitive": false } }, + { "type": "contains", "value": "vw", "message": "Use 'vw' unit for fluid sizing", "options": { "caseSensitive": false } }, + { "type": "property_value", "value": { "property": "font-size", "expected": "5vw" }, "message": "Set font-size to '5vw'" } + ] + }, + { + "id": "responsive-3", + "title": "Flexible Grids", + "description": "Combine CSS Grid with auto-fit or auto-fill for responsive column layouts.", + "task": "Define '.grid-responsive' with grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); and a gap of 1rem.", + "previewHTML": "
1
2
3
4
", + "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .grid-responsive > div { background: #d1c4e9; padding: 1rem; }", + "sandboxCSS": "", + "codePrefix": "/* Create a responsive grid */\n.grid-responsive {", + "initialCode": "", + "codeSuffix": "}", + "previewContainer": "preview-area", + "validations": [ + { + "type": "contains", + "value": "grid-template-columns", + "message": "Define 'grid-template-columns'", + "options": { "caseSensitive": false } + }, + { + "type": "regex", + "value": "repeat\\(auto-fit,\\s*minmax\\(200px,\\s*1fr\\)\\)", + "message": "Use repeat(auto-fit, minmax(200px, 1fr))", + "options": { "caseSensitive": false } + }, + { "type": "contains", "value": "gap", "message": "Use 'gap' property", "options": { "caseSensitive": false } } + ] + }, + { + "id": "responsive-4", + "title": "Mobile-First Media Queries", + "description": "Adopt a mobile-first approach by writing base styles for small screens and enhancing for larger viewports.", + "task": "Write a media query for min-width 768px that sets '.sidebar' width to 250px.", + "previewHTML": "", + "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .sidebar { background: #c8e6c9; padding: 1rem; }", + "sandboxCSS": "", + "codePrefix": "/* Add mobile-first enhancement */\n", + "initialCode": "", + "codeSuffix": "", + "previewContainer": "preview-area", + "validations": [ + { + "type": "regex", + "value": "@media\\s*\\(min-width:\\s*768px\\)", + "message": "Use a media query for min-width: 768px", + "options": { "caseSensitive": false } + }, + { "type": "contains", "value": ".sidebar", "message": "Target '.sidebar' inside media query", "options": { "caseSensitive": false } }, + { + "type": "property_value", + "value": { "property": "width", "expected": "250px" }, + "message": "Set width to '250px'", + "options": { "exact": false } + } + ] + } + ] +} diff --git a/public/bar_1680535.png b/public/bar_1680535.png new file mode 100644 index 0000000000000000000000000000000000000000..7930534e0e0ba36e18bc546e8c17ac2cf00e9f2a GIT binary patch literal 3809 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"; + container.innerHTML = "

CSS Lessons

"; // Create list items for each module modules.forEach((module) => { @@ -86,16 +86,18 @@ export function showFeedback(isSuccess, message) { feedbackElement.textContent = message; // Find where to insert the feedback - const insertAfter = document.querySelector(".code-editor"); + const insertAfter = document.querySelector(".editor-content"); if (insertAfter && insertAfter.parentNode) { insertAfter.parentNode.insertBefore(feedbackElement, insertAfter.nextSibling); } - // Auto-remove feedback after some time if successful - if (isSuccess) { + if (!isSuccess) { setTimeout(() => { - clearFeedback(); - }, 5000); + if (feedbackElement && feedbackElement.parentNode) { + feedbackElement.parentNode.removeChild(feedbackElement); + } + feedbackElement = null; + }, 3_000); // Remove feedback after 3 seconds } } diff --git a/src/index.html b/src/index.html index b3f4bab..acabe5e 100644 --- a/src/index.html +++ b/src/index.html @@ -11,7 +11,8 @@