feat: add complete German translation of the website
- Add German HTML entry point (index.de.html) - Add German app.js with translated UI strings (app.de.js) - Add German lesson config (lessons.de.js) - Add German translations of all 6 lesson modules: - HTML Elements: Block vs Inline - HTML Forms: Basic Inputs - HTML Forms: Validation - CSS: Basic Selectors - CSS: Advanced Selectors - Tailwind: Basics All IDs, variable names, and code examples remain in English. Only user-facing text has been translated to German.
This commit is contained in:
550
lessons/de/00-basic-selectors.json
Normal file
550
lessons/de/00-basic-selectors.json
Normal file
@@ -0,0 +1,550 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "css-basic-selectors",
|
||||||
|
"title": "CSS: Grundlegende Selektoren",
|
||||||
|
"description": "CSS-Selektoren sind die Grundlage für das Stylen von Webseiten und ermöglichen es dir, bestimmte HTML-Elemente für die Gestaltung auszuwählen. Dieses Modul stellt grundlegende Selektortypen vor, einschließlich Element-Typ-Selektoren, Klassen-Selektoren, ID-Selektoren und des universellen Selektors.",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "introduction-to-selectors",
|
||||||
|
"title": "Was ist ein CSS-Selektor?",
|
||||||
|
"description": "Ein CSS-Selektor ist der erste Teil einer CSS-Regel, der dem Browser mitteilt, welche HTML-Elemente die im Deklarationsblock definierten Stile erhalten sollen. Selektoren sind im Wesentlichen Muster, die mit Elementen in deinem HTML-Dokument übereinstimmen. Das Verstehen von Selektoren ist grundlegend, da sie bestimmen, welche Elemente von deinen CSS-Regeln betroffen sind. Das Element oder die Elemente, die von einem Selektor anvisiert werden, werden als 'Subjekt des Selektors' bezeichnet. Beim Schreiben einer CSS-Regel gibst du zuerst den Selektor an, gefolgt von geschweiften Klammern, die die Stil-Deklarationen enthalten.<br/>Um beispielsweise die Textfarbe von Elementen zu ändern, kannst du die <kbd>color</kbd>-Eigenschaft in deinem Deklarationsblock verwenden.<br><br><pre>/* Element-Selektor */\np {\n color: orangered;\n /* │ └─── Gibt den Wert des Ausdrucks an\n │ \n └─────────── Gibt die Eigenschaft des Ausdrucks an */\n}</pre>",
|
||||||
|
"task": "Schreibe eine CSS-Regel mit einem Typ-Selektor, die alle Absatz-Elemente <kbd>p</kbd> im Dokument anvisiert. Mache den Text blau, indem du die <kbd>color</kbd>-Eigenschaft auf <kbd>blue</kbd> setzt.",
|
||||||
|
"previewHTML": "<h1>Einführung in CSS-Selektoren</h1>\n<p>Dieser Absatz sollte blau werden.</p>\n<div>Dieses div-Element sollte unverändert bleiben.</div>\n<p>Dieser zweite Absatz sollte ebenfalls blau werden.</p>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
|
||||||
|
"sandboxCSS": "h1, p, div { padding: 8px; margin-bottom: 10px; border: 1px dashed #ccc; }",
|
||||||
|
"codePrefix": "/* Schreibe einen Typ-Selektor, um alle Absatz-Elemente anzuvisieren */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"solution": "p { color: blue }",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^p\\s*{",
|
||||||
|
"message": "Beginne deine Regel mit <kbd>p { … }</kbd>, um alle Absatz-Elemente auszuwählen",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "color:",
|
||||||
|
"message": "Füge die <kbd>color:</kbd>-Eigenschaft in deine CSS-Regel ein"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "blue",
|
||||||
|
"message": "Setze den Farbwert auf <kbd>blue</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "color",
|
||||||
|
"expected": "blue"
|
||||||
|
},
|
||||||
|
"message": "Verwende <kbd>color: blue</kbd>, um die Textfarbe zu setzen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "p\\s*{[^}]*}",
|
||||||
|
"message": "Vergiss nicht, deine CSS-Regel mit einer schließenden Klammer <kbd>}</kbd> zu beenden",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "type-selectors",
|
||||||
|
"title": "Typ-Selektoren: HTML-Elemente anvisieren",
|
||||||
|
"description": "Typ-Selektoren (auch Tag-Name-Selektoren oder Element-Selektoren genannt) visieren HTML-Elemente basierend auf ihrem Tag-Namen an. Zum Beispiel wählt <kbd>p</kbd> alle Absatz-Elemente, <kbd>h1</kbd> alle Überschriften der ersten Ebene und <kbd>div</kbd> alle Division-Elemente aus. Typ-Selektoren sind die grundlegendste Art, Elemente auszuwählen, und wenden Stile einheitlich auf alle Instanzen eines bestimmten HTML-Elements in deinem Dokument an. Du kannst verschiedene CSS-Eigenschaften mit Typ-Selektoren definieren, wie <kbd>color</kbd> für Textfarbe, <kbd>background-color</kbd> für den Hintergrund und <kbd>font-weight</kbd> für Textbetonung. Sie bieten einen breiten Ansatz für das Styling deiner Seite und sind oft der Ausgangspunkt für spezifischeres Styling mit anderen Selektortypen.",
|
||||||
|
"task": "Schreibe drei separate CSS-Regeln mit Typ-Selektoren, um bestimmte HTML-Elemente anzuvisieren: mache <kbd>h2</kbd>-Überschriften <kbd>purple</kbd>, gib <kbd>span</kbd>-Elementen einen <kbd>yellow</kbd>-Hintergrund und mache <kbd>strong</kbd>-Elemente <kbd>red</kbd>.",
|
||||||
|
"previewHTML": "<h2>Typ-Selektoren Beispiel</h2>\n<p>Normaler Absatztext <span>mit einem hervorgehobenen Span</span>, der einen gelben Hintergrund haben sollte.</p>\n<p>Ein weiterer Absatz mit <strong>wichtigem, fetten Text</strong>, der rot sein sollte.</p>\n<h2>Eine weitere Überschrift</h2>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
|
||||||
|
"sandboxCSS": "h2, p, span, strong { padding: 3px; }",
|
||||||
|
"codePrefix": "/* Schreibe drei separate Typ-Selektoren unten */\n\n",
|
||||||
|
"initialCode": "/* 1. Mache h2-Überschriften lila */\n\n\n/* 2. Gib span-Elementen einen gelben Hintergrund */\n\n\n/* 3. Mache strong-Elemente rot */\n",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"solution": "/* 1. Mache h2-Überschriften lila */\nh2 {\n color: purple;\n}\n\n/* 2. Gib span-Elementen einen gelben Hintergrund */\nspan {\n background-color: yellow;\n}\n\n/* 3. Mache strong-Elemente rot */\nstrong {\n color: red;\n}",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^h2\\s*{",
|
||||||
|
"message": "Füge einen <kbd>h2 { … }</kbd>-Selektor hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "color",
|
||||||
|
"expected": "purple"
|
||||||
|
},
|
||||||
|
"message": "Setze die <kbd>color</kbd>-Eigenschaft auf <kbd>purple</kbd> für h2-Elemente"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "h2\\s*{[^}]*}",
|
||||||
|
"message": "Vergiss nicht, deine h2-Regel mit einer schließenden Klammer <kbd>}</kbd> zu beenden"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^span\\s*{",
|
||||||
|
"message": "Füge einen <kbd>span { … }</kbd>-Selektor hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "background-color",
|
||||||
|
"expected": "yellow"
|
||||||
|
},
|
||||||
|
"message": "Setze <kbd>background-color: yellow</kbd> für span-Elemente"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "span\\s*{[^}]*}",
|
||||||
|
"message": "Vergiss nicht, deine span-Regel mit einer schließenden Klammer <kbd>}</kbd> zu beenden"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^strong\\s*{",
|
||||||
|
"message": "Füge einen <kbd>strong { … }</kbd>-Selektor hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "strong\\s*{\\s*color:\\s*red;[^}]*}",
|
||||||
|
"message": "Setze <kbd>color: red</kbd> für strong-Elemente"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "class-selectors",
|
||||||
|
"title": "Klassen-Selektoren: Elementgruppen stylen",
|
||||||
|
"description": "Klassen-Selektoren visieren Elemente mit einem bestimmten Klassen-Attributwert an. Sie beginnen mit einem Punkt (.) gefolgt vom Klassennamen. Klassen sind mächtig, weil sie es ermöglichen, die gleichen Stile auf mehrere Elemente anzuwenden, unabhängig von ihrem Typ. Ein HTML-Element kann mehrere Klassen haben (durch Leerzeichen im class-Attribut getrennt), und eine Klasse kann auf beliebig viele Elemente angewendet werden. Bei der Verwendung von Klassen-Selektoren kannst du Eigenschaften wie <kbd>background-color</kbd> zum Setzen der Hintergrundfarbe und <kbd>font-weight</kbd> zur Kontrolle der Textdicke anwenden. Diese Flexibilität macht Klassen-Selektoren zu einer der am häufigsten verwendeten Methoden zur Anwendung von Stilen in CSS und ermöglicht modulares und wiederverwendbares Styling auf deiner gesamten Website.",
|
||||||
|
"task": "Erstelle eine CSS-Regel mit einem Klassen-Selektor, die Elemente mit der Klasse <kbd>highlight</kbd> anvisiert. Gib diesen Elementen einen <kbd>yellow</kbd>-Hintergrund und <kbd>bold</kbd>-Text.",
|
||||||
|
"previewHTML": "<h2>Verwendung von Klassen-Selektoren</h2>\n<p>Dies ist ein normaler Absatz, aber <span class=\"highlight\">dieser Span hat die highlight-Klasse</span> angewendet.</p>\n<p class=\"highlight\">Dieser gesamte Absatz hat die highlight-Klasse.</p>\n<ul>\n <li>Normales Listenelement</li>\n <li class=\"highlight\">Dieses Listenelement ist hervorgehoben</li>\n</ul>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
|
||||||
|
"sandboxCSS": "h2, p, li { padding: 5px; margin-bottom: 10px; }",
|
||||||
|
"codePrefix": "/* Erstelle einen Klassen-Selektor für Elemente mit der 'highlight'-Klasse */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^\\.highlight\\s*{",
|
||||||
|
"message": "Beginne deine Regel mit <kbd>.highlight { … }</kbd>, um einen Klassen-Selektor zu erstellen",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "background-color:",
|
||||||
|
"message": "Füge die <kbd>background-color:</kbd>-Eigenschaft hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "background-color",
|
||||||
|
"expected": "yellow"
|
||||||
|
},
|
||||||
|
"message": "Setze die Hintergrundfarbe auf <kbd>yellow</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "font-weight:",
|
||||||
|
"message": "Füge die <kbd>font-weight:</kbd>-Eigenschaft hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "font-weight",
|
||||||
|
"expected": "bold"
|
||||||
|
},
|
||||||
|
"message": "Setze font-weight auf <kbd>bold</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "\\.highlight\\s*{[^}]*}",
|
||||||
|
"message": "Vergiss nicht, deine CSS-Regel mit einer schließenden Klammer <kbd>}</kbd> zu beenden",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "multiple-classes",
|
||||||
|
"title": "Mit mehreren Klassen arbeiten",
|
||||||
|
"description": "HTML-Elemente können mehrere Klassen gleichzeitig haben, was komponierbare und modulare CSS-Designs ermöglicht. Wenn ein Element mehrere Klassen hat, erhält es Stile von allen passenden Klassen-Selektoren. Dieser Ansatz ermöglicht es dir, eine Bibliothek wiederverwendbarer CSS-Klassen aufzubauen, die auf verschiedene Arten kombiniert werden können. Du kannst auch Elemente anvisieren, die eine bestimmte Kombination von Klassen haben, indem du Klassen-Selektoren ohne Leerzeichen verkettst (z.B. <kbd>.class1.class2</kbd>). Beim Stylen dieser Elemente könntest du Eigenschaften wie <kbd>border-color</kbd> verwenden, um die Farbe von Element-Rahmen zu ändern, und <kbd>background-color</kbd>, um die Hintergrundfarbe von Elementen zu setzen. Diese Technik ermöglicht bedingte Stile, die nur gelten, wenn bestimmte Klassen zusammen erscheinen.",
|
||||||
|
"task": "Vervollständige die CSS-Regel, die Elemente mit sowohl <kbd>card</kbd> als auch <kbd>featured</kbd>-Klassen anvisiert, indem du die Selektoren verkettest. Setze border-color auf gold und background-color auf lemonchiffon, um hervorgehobene Karten hervorzuheben.",
|
||||||
|
"previewHTML": "<h2>Mehrfach-Klassen-Kombinationen</h2>\n<div class=\"card\">Normale Karte</div>\n<div class=\"card featured\">Hervorgehobene Karte</div>\n<div class=\"featured\">Nur hervorgehoben (keine Karte)</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; } .card { border: 2px solid gray; padding: 15px; margin-bottom: 10px; border-radius: 5px; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Die .card-Klasse hat bereits grundlegendes Styling */\n/* Visiere jetzt Elemente mit BEIDEN Klassen an: 'card' UND 'featured' */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"solution": ".card.featured { border-color: gold; background-color: lemonchiffon }",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^\\.card\\.featured\\s*{",
|
||||||
|
"message": "Verkette die Selektoren als <kbd>.card.featured</kbd> (kein Leerzeichen dazwischen)",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "border-color:",
|
||||||
|
"message": "Füge die <kbd>border-color</kbd>-Eigenschaft hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "border-color",
|
||||||
|
"expected": "gold"
|
||||||
|
},
|
||||||
|
"message": "Setze die Rahmenfarbe auf <kbd>gold</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "\\.card\\.featured\\s*{[^}]*;",
|
||||||
|
"message": "Vergiss nicht, deine CSS-Regel mit einem Semikolon <kbd>;</kbd> zu beenden"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "background-color:",
|
||||||
|
"message": "Füge die <kbd>background-color</kbd>-Eigenschaft hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "background-color",
|
||||||
|
"expected": "lemonchiffon"
|
||||||
|
},
|
||||||
|
"message": "Setze die Hintergrundfarbe auf <kbd>lemonchiffon</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "\\.card\\.featured\\s*{[^}]*}",
|
||||||
|
"message": "Vergiss nicht, deine CSS-Regel mit einer schließenden Klammer <kbd>}</kbd> zu beenden",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "class-with-type",
|
||||||
|
"title": "Typ- und Klassen-Selektoren kombinieren",
|
||||||
|
"description": "Du kannst Typ-Selektoren mit Klassen-Selektoren kombinieren, um bestimmte HTML-Elemente anzuvisieren, die eine bestimmte Klasse haben. Dies erstellt einen spezifischeren Selektor, der nur passt, wenn beide Bedingungen wahr sind: das Element ist vom angegebenen Typ UND es hat die angegebene Klasse. Zum Beispiel würde <kbd>p.note</kbd> Absatz-Elemente mit der Klasse <kbd>note</kbd> auswählen, aber keine divs oder spans mit derselben Klasse. Du kannst diese kombinierten Selektionen mit Eigenschaften wie <kbd>background-color</kbd> stylen, um einen farbigen Hintergrund für deine Elemente zu setzen. Dieser Ansatz ermöglicht es dir, verschiedene Stile auf dieselbe Klasse anzuwenden, wenn sie auf verschiedenen Elementtypen erscheint.",
|
||||||
|
"task": "Erstelle eine CSS-Regel, die speziell <kbd><span></kbd>-Elemente mit der Klasse <kbd>highlight</kbd> anvisiert. Gib diesen Elementen einen orangen Hintergrund, während andere Elemente mit der highlight-Klasse unverändert bleiben.",
|
||||||
|
"previewHTML": "<h2>Typ- und Klassen-Kombinationen</h2>\n<p>Dieser Absatz hat einen <span class=\"highlight\">hervorgehobenen Span</span>, der einen orangen Hintergrund haben sollte.</p>\n<p class=\"highlight\">Dieser Absatz hat die highlight-Klasse, sollte aber KEINEN orangen Hintergrund haben.</p>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; } .highlight { font-weight: bold; }",
|
||||||
|
"sandboxCSS": "h2, p, span { padding: 5px; }",
|
||||||
|
"codePrefix": "/* Die .highlight-Klasse setzt bereits font-weight auf bold */\n/* Visiere jetzt NUR span-Elemente mit der highlight-Klasse an */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^span\\.highlight\\s*{",
|
||||||
|
"message": "Verwende den <kbd>span.highlight</kbd>-Selektor (kein Leerzeichen zwischen Element und Klasse)",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "background-color:",
|
||||||
|
"message": "Füge die <kbd>background-color</kbd>-Eigenschaft hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "background-color",
|
||||||
|
"expected": "orange"
|
||||||
|
},
|
||||||
|
"message": "Setze die Hintergrundfarbe auf <kbd>orange</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "span\\.highlight\\s*{[^}]*}",
|
||||||
|
"message": "Vergiss nicht, deine CSS-Regel mit einer schließenden Klammer <kbd>}</kbd> zu beenden",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "id-selectors",
|
||||||
|
"title": "ID-Selektoren: Einzigartige Elemente anvisieren",
|
||||||
|
"description": "ID-Selektoren visieren Elemente mit einem bestimmten id-Attribut an. Sie beginnen mit einem Raute-/Hash-Zeichen (#) gefolgt vom ID-Namen. Im Gegensatz zu Klassen müssen IDs innerhalb eines Dokuments einzigartig sein – jeder ID-Wert sollte nur einmal pro Seite verwendet werden. ID-Selektoren haben eine höhere Spezifität als Klassen- oder Element-Selektoren, was bedeutet, dass sie diese Selektoren bei Konflikten überschreiben. Beim Stylen mit ID-Selektoren kannst du Eigenschaften wie <kbd>color</kbd> verwenden, um die Textfarbe zu definieren, und <kbd>text-decoration</kbd>, um das Erscheinungsbild von Text zu kontrollieren, wie das Hinzufügen von Unterstreichungen zu Elementen. Wegen ihrer Einzigartigkeitsanforderung werden IDs am besten für einmalige Elemente wie Seitenköpfe, Hauptnavigation oder spezifische einzigartige Komponenten verwendet, die nur einmal auf einer Seite erscheinen.",
|
||||||
|
"task": "Erstelle eine CSS-Regel mit einem ID-Selektor, die das Element mit der ID <kbd>main-title</kbd> anvisiert. Setze seine Farbe auf purple und füge eine Unterstreichung mit <kbd>text-decoration: underline</kbd> hinzu.",
|
||||||
|
"previewHTML": "<h1 id=\"main-title\">Haupt-Seitentitel</h1>\n<p>Normaler Absatzinhalt.</p>\n<h2>Sekundäre Überschrift</h2>\n<p id=\"intro\">Einführungsabsatz (andere ID).</p>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
|
||||||
|
"sandboxCSS": "h1, h2, p { padding: 8px; margin-bottom: 10px; border: 1px dashed #ccc; }",
|
||||||
|
"codePrefix": "/* Erstelle einen ID-Selektor für das Element mit id=\"main-title\" */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^#main-title\\s*{",
|
||||||
|
"message": "Beginne deine Regel mit <kbd>#main-title</kbd>, um einen ID-Selektor zu erstellen",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "color:",
|
||||||
|
"message": "Füge die <kbd>color</kbd>-Eigenschaft hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "color",
|
||||||
|
"expected": "purple"
|
||||||
|
},
|
||||||
|
"message": "Setze die Farbe auf <kbd>purple</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "text-decoration:",
|
||||||
|
"message": "Füge die <kbd>text-decoration</kbd>-Eigenschaft hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "text-decoration",
|
||||||
|
"expected": "underline"
|
||||||
|
},
|
||||||
|
"message": "Setze text-decoration auf <kbd>underline</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "#main-title\\s*{[^}]*}",
|
||||||
|
"message": "Vergiss nicht, deine CSS-Regel mit einer schließenden Klammer <kbd>}</kbd> zu beenden",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "id-with-type",
|
||||||
|
"title": "Typ- und ID-Selektoren kombinieren",
|
||||||
|
"description": "Ähnlich wie du Typ- und Klassen-Selektoren kombinieren kannst, kannst du auch Typ-Selektoren mit ID-Selektoren kombinieren. Zum Beispiel visiert <kbd>h1#title</kbd> ein h1-Element mit der ID 'title' an. Bei diesem kombinierten Ansatz kannst du CSS-Eigenschaften wie <kbd>font-style</kbd> anwenden, um die Neigung des Textes zu kontrollieren und ihn kursiv oder normal zu machen. Obwohl diese Selektor-Kombination spezifischer ist als die Verwendung nur des ID-Selektors, ist sie oft unnötig, da IDs bereits einzigartig im Dokument sein sollten. Diese Technik kann jedoch nützlich sein, um die Lesbarkeit des Codes zu verbessern oder wenn du betonen möchtest, dass eine bestimmte ID nur auf einem bestimmten Elementtyp erscheinen sollte.",
|
||||||
|
"task": "Erstelle eine CSS-Regel, die einen Typ-Selektor mit einem ID-Selektor kombiniert, um speziell ein Absatz-Element mit der ID <kbd>special</kbd> anzuvisieren. Setze seinen Schriftstil auf kursiv.",
|
||||||
|
"previewHTML": "<h2 id=\"special\">Überschrift mit ID \"special\" (sollte NICHT betroffen sein)</h2>\n<p id=\"special\">Absatz mit ID \"special\" (sollte kursiv werden)</p>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
|
||||||
|
"sandboxCSS": "h2, p { padding: 8px; margin-bottom: 10px; border: 1px dashed #ccc; }",
|
||||||
|
"codePrefix": "/* Erstelle einen kombinierten Typ+ID-Selektor für einen Absatz mit id=\"special\" */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^p#special\\s*{",
|
||||||
|
"message": "Verwende <kbd>p#special</kbd>, um Absätze mit ID 'special' anzuvisieren",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "font-style:",
|
||||||
|
"message": "Füge die <kbd>font-style</kbd>-Eigenschaft hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "font-style",
|
||||||
|
"expected": "italic"
|
||||||
|
},
|
||||||
|
"message": "Setze font-style auf <kbd>italic</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "p#special\\s*{[^}]*}",
|
||||||
|
"message": "Vergiss nicht, deine CSS-Regel mit einer schließenden Klammer <kbd>}</kbd> zu beenden",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "selector-lists",
|
||||||
|
"title": "Selektor-Listen: Gleiche Regeln auf mehrere Selektoren anwenden",
|
||||||
|
"description": "Wenn mehrere Elemente das gleiche Styling benötigen, kannst du sie mit einer Selektor-Liste (auch Gruppierungs-Selektoren genannt) zusammenfassen. Selektor-Listen werden erstellt, indem einzelne Selektoren durch Kommas getrennt werden. Dieser Ansatz reduziert Wiederholungen in deinem CSS und macht es wartbarer und effizienter. Zum Beispiel wendet <kbd>h1, h2, h3 { color: blue; }</kbd> die gleiche blaue Farbe auf alle drei Überschriftenebenen an. Beim Stylen mehrerer Selektoren gleichzeitig kannst du Eigenschaften wie <kbd>background-color</kbd> für den Hintergrund, <kbd>border-left</kbd> für einen linken Rahmen mit bestimmter Dicke, Stil und Farbe, und <kbd>padding-left</kbd> anwenden, um Abstand zwischen dem Inhalt und dem linken Rahmen zu schaffen. Leerzeichen um Kommas sind optional, und jeder Selektor in der Liste kann ein beliebiger gültiger Selektortyp sein – Elemente, Klassen, IDs oder sogar komplexere Selektoren.",
|
||||||
|
"task": "Erstelle eine Selektor-Liste, die die gleichen Stile auf drei verschiedene Elemente anwendet: Absätze mit der Klasse <kbd>note</kbd>, Listenelemente mit der Klasse <kbd>important</kbd> und das Element mit der ID <kbd>summary</kbd>. Gib ihnen einen <kbd>lightyellow</kbd>-Hintergrund, einen <kbd>gold</kbd>-linken Rahmen und etwas linkes <kbd>padding</kbd>.",
|
||||||
|
"previewHTML": "<p class=\"note\">Dies ist ein Notiz-Absatz.</p>\n<ul>\n <li>Normales Listenelement</li>\n <li class=\"important\">Wichtiges Listenelement</li>\n</ul>\n<div id=\"summary\">Zusammenfassungs-Abschnitt</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
|
||||||
|
"sandboxCSS": "p, li, div { padding: 8px; margin-bottom: 8px; border: 1px dashed gray; }",
|
||||||
|
"codePrefix": "/* Erstelle eine Selektor-Liste, um die gleichen Stile auf mehrere verschiedene Elemente anzuwenden */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"solution": "p.note,\nli.important,\n#summary {\n background-color: lightyellow;\n border-left: 3px solid gold;\n padding-left: 10px\n}",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "p.note",
|
||||||
|
"message": "Füge <kbd>p.note</kbd> in deine Selektor-Liste ein",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "li.important",
|
||||||
|
"message": "Füge <kbd>li.important</kbd> in deine Selektor-Liste ein",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "#summary",
|
||||||
|
"message": "Füge <kbd>#summary</kbd> in deine Selektor-Liste ein",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "(p\\.note|li\\.important|#summary)\\s*,\\s*(p\\.note|li\\.important|#summary)\\s*,\\s*(p\\.note|li\\.important|#summary)",
|
||||||
|
"message": "Erstelle eine kommagetrennte Liste mit allen drei Selektoren",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "background-color:",
|
||||||
|
"message": "Füge die <kbd>background-color</kbd>-Eigenschaft hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "background-color",
|
||||||
|
"expected": "lightyellow"
|
||||||
|
},
|
||||||
|
"message": "Setze die Hintergrundfarbe auf <kbd>lightyellow</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "border-left:",
|
||||||
|
"message": "Füge die <kbd>border-left</kbd>-Eigenschaft hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "border-left",
|
||||||
|
"expected": "3px solid gold"
|
||||||
|
},
|
||||||
|
"message": "Verwende <kbd>border-left: 3px solid gold</kbd>, um einen linken Rahmen zu erstellen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "padding-left:",
|
||||||
|
"message": "Füge die <kbd>padding-left</kbd>-Eigenschaft hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "padding-left",
|
||||||
|
"expected": "10px"
|
||||||
|
},
|
||||||
|
"message": "Verwende <kbd>padding-left: 10px</kbd>, um linkes Padding hinzuzufügen"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "universal-selector",
|
||||||
|
"title": "Der universelle Selektor: Alles anvisieren",
|
||||||
|
"description": "Der universelle Selektor wird durch ein Sternchen (*) gekennzeichnet und passt auf jedes Element jedes Typs. Er wählt alles im Dokument aus oder, wenn er mit anderen Selektoren kombiniert wird, alles innerhalb eines bestimmten Kontexts. Zum Beispiel entfernt <kbd>* { margin: 0; }</kbd> Ränder von allen Elementen, während <kbd>article *</kbd> alle Elemente innerhalb von article-Elementen auswählt. Bei der Verwendung des universellen Selektors in Kombination mit anderen Selektoren kannst du Eigenschaften wie <kbd>margin</kbd> anwenden, um die Abstände um Elemente zu kontrollieren. Der universelle Selektor ist mächtig, sollte aber wegen seiner breiten Auswirkung vorsichtig verwendet werden. Er wird häufig in CSS-Resets verwendet, um Standard-Browser-Styling zu überschreiben, oder um alle Kinder eines bestimmten Elements anzuvisieren.",
|
||||||
|
"task": "Verwende den universellen Selektor, um Ränder von allen Elementen innerhalb des Container-divs zu entfernen. Erstelle eine Regel mit <kbd>div.container *</kbd> als Selektor und setze <kbd>margin: 0</kbd>.",
|
||||||
|
"previewHTML": "<div class=\"container\">\n <h2>Innerhalb des Containers</h2>\n <p>Dieser Absatz ist innerhalb des Containers.</p>\n <ul>\n <li>Listenelement innerhalb des Containers</li>\n </ul>\n</div>\n<p>Dieser Absatz ist außerhalb des Containers und sollte nicht betroffen sein.</p>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; } div.container { border: 2px solid navy; padding: 15px; background-color: lavender; } h2, p, ul, li { margin: 15px 0; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Verwende den universellen Selektor, um alle Elemente innerhalb des Containers anzuvisieren */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^div\\.container\\s+\\*\\s*{",
|
||||||
|
"message": "Verwende <kbd>div.container *</kbd>-Selektor (mit einem Leerzeichen zwischen container und *)",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "margin:",
|
||||||
|
"message": "Füge die <kbd>margin</kbd>-Eigenschaft hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "margin",
|
||||||
|
"expected": "0"
|
||||||
|
},
|
||||||
|
"message": "Setze margin auf <kbd>0</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "div\\.container\\s+\\*\\s*{[^}]*}",
|
||||||
|
"message": "Vergiss nicht, deine CSS-Regel mit einer schließenden Klammer <kbd>}</kbd> zu beenden",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "specificity-basics",
|
||||||
|
"title": "Selektor-Spezifität verstehen",
|
||||||
|
"description": "CSS-Spezifität bestimmt, welche Stile Vorrang haben, wenn mehrere widersprüchliche Regeln auf dasselbe Element abzielen. Spezifität folgt einem hierarchischen System: Inline-Stile haben die höchste Spezifität, gefolgt von ID-Selektoren, dann Klassen-/Attribut-/Pseudo-Klassen-Selektoren und schließlich Element-/Pseudo-Element-Selektoren. Dies kann als vierteilige Punktzahl (inline, ID, Klasse, Element) konzeptualisiert werden. Beim Erstellen mehrerer Regeln, die auf dieselben Elemente abzielen könnten, kannst du die <kbd>color</kbd>-Eigenschaft verwenden, um Textfarben zu setzen, und die Spezifität bestimmt, welche Farbe tatsächlich angewendet wird. Das Verstehen von Spezifität ist entscheidend für vorhersagbares Styling und das Debuggen von CSS-Konflikten. Wenn zwei Selektoren gleiche Spezifität haben, gewinnt derjenige, der zuletzt im Stylesheet kommt.",
|
||||||
|
"task": "Untersuche die vorhandenen CSS-Regeln und füge eine neue Regel mit höherer Spezifität hinzu, um die Textfarbe des Absatzes zu überschreiben. Erstelle eine Regel mit '.content p' als Selektor und setze color: green.",
|
||||||
|
"previewHTML": "<div class=\"content\">\n <p>Welche Farbe wird dieser Absatz haben? Schau dir die CSS-Regeln und ihre Spezifität an.</p>\n</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
|
||||||
|
"sandboxCSS": "p { border: 1px dashed gray; padding: 10px; }",
|
||||||
|
"codePrefix": "/* Diese CSS-Regeln visieren denselben Absatz an, haben aber unterschiedliche Spezifität */\n\n/* Regel 1: Element-Selektor (niedrigste Spezifität) */\np {\n color: red;\n}\n\n/* Regel 2: Nachfahren-Selektor (höhere Spezifität als nur 'p') */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^\\.content\\s+p\\s*{",
|
||||||
|
"message": "Verwende <kbd>.content p</kbd> als deinen Selektor (beachte das Leerzeichen dazwischen)",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "color:",
|
||||||
|
"message": "Füge die <kbd>color</kbd>-Eigenschaft hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "green",
|
||||||
|
"message": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
478
lessons/de/01-advanced-selectors.json
Normal file
478
lessons/de/01-advanced-selectors.json
Normal file
@@ -0,0 +1,478 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "css-advanced-selectors",
|
||||||
|
"title": "CSS: Fortgeschrittene Selektoren",
|
||||||
|
"description": "Meistere fortgeschrittene CSS-Selektor-Techniken einschließlich Attribut-Selektoren, Kombinatoren und Pseudo-Klassen. Dieses Modul baut auf grundlegenden Selektoren auf und gibt dir präzise Kontrolle über die Elementauswahl, was ausgefeilte Styling-Muster und interaktive Effekte ermöglicht.",
|
||||||
|
"difficulty": "intermediate",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "attribute-selectors",
|
||||||
|
"title": "Attribut-Selektoren: Anvisieren nach HTML-Attributen",
|
||||||
|
"description": "Attribut-Selektoren ermöglichen es dir, Elemente basierend auf ihren HTML-Attributen und Attributwerten anzuvisieren. Sie sind unglaublich mächtig für das Styling von Formularen, Links und anderen Elementen mit spezifischen Attributen. Die grundlegende Syntax verwendet eckige Klammern: <kbd>[attribute]</kbd> wählt Elemente mit diesem Attribut, <kbd>[attribute=\"value\"]</kbd> wählt Elemente, bei denen das Attribut genau diesem Wert entspricht, und <kbd>[attribute^=\"value\"]</kbd> wählt Elemente, bei denen das Attribut mit diesem Wert beginnt. Du kannst diese ausgewählten Elemente mit Eigenschaften wie <kbd>border</kbd> stylen, um visuelle Grenzen hinzuzufügen, und <kbd>background-color</kbd>, um bestimmte Formularfelder oder Links hervorzuheben.",
|
||||||
|
"task": "Erstelle eine CSS-Regel mit einem Attribut-Selektor, die alle input-Elemente mit <kbd>type=\"text\"</kbd> anvisiert. Gib ihnen einen <kbd>lightblue</kbd>-Hintergrund und einen <kbd>2px solid blue</kbd>-Rahmen.",
|
||||||
|
"previewHTML": "<form>\n <p><label>Name: <input type=\"text\" placeholder=\"Gib deinen Namen ein\"></label></p>\n <p><label>E-Mail: <input type=\"email\" placeholder=\"Gib deine E-Mail ein\"></label></p>\n <p><label>Passwort: <input type=\"password\" placeholder=\"Passwort eingeben\"></label></p>\n <p><button type=\"submit\">Absenden</button></p>\n</form>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; } form { max-width: 300px; } label { display: block; margin-bottom: 5px; } input, button { padding: 8px; margin-bottom: 10px; border-radius: 4px; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Visiere input-Elemente mit type=\"text\" mit einem Attribut-Selektor an */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"solution": "input[type=\"text\"] {\n background-color: lightblue;\n border: 2px solid blue\n}",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^input\\[type=\"text\"\\]\\s*{",
|
||||||
|
"message": "Verwende <kbd>input[type=\"text\"] { … }</kbd> als deinen Attribut-Selektor",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "background-color:",
|
||||||
|
"message": "Füge die <kbd>background-color</kbd>-Eigenschaft hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "background-color",
|
||||||
|
"expected": "lightblue"
|
||||||
|
},
|
||||||
|
"message": "Setze die Hintergrundfarbe auf <kbd>lightblue</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "background-color:\\s*[^;]*;",
|
||||||
|
"message": "Vergiss nicht, die <kbd>background-color</kbd>-Deklaration mit einem Semikolon <kbd>;</kbd> zu beenden"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "border:",
|
||||||
|
"message": "Füge die <kbd>border</kbd>-Eigenschaft hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "border",
|
||||||
|
"expected": "2px solid blue"
|
||||||
|
},
|
||||||
|
"message": "Setze den Rahmen auf <kbd>2px solid blue</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "input\\[type=\"text\"\\]\\s*{[^}]*}",
|
||||||
|
"message": "Vergiss nicht, deine CSS-Regel mit einer schließenden Klammer <kbd>}</kbd> zu beenden",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "attribute-partial-matching",
|
||||||
|
"title": "Teilweises Attribut-Matching",
|
||||||
|
"description": "Attribut-Selektoren unterstützen teilweise Matching-Muster, die es dir ermöglichen, Elemente basierend auf Teilen von Attributwerten anzuvisieren. Der <kbd>[attribute^=\"value\"]</kbd>-Selektor passt auf Elemente, bei denen das Attribut mit dem angegebenen Wert beginnt, <kbd>[attribute$=\"value\"]</kbd> passt, wenn es mit dem Wert endet, und <kbd>[attribute*=\"value\"]</kbd> passt, wenn der Wert irgendwo innerhalb des Attributs erscheint. Diese Muster sind besonders nützlich für das Styling externer Links, Dateitypen oder Elemente mit Klassennamen, die Namenskonventionen folgen. Beim Stylen dieser gematchten Elemente kannst du Eigenschaften wie <kbd>color</kbd> verwenden, um die Textfarbe zu ändern, und <kbd>text-decoration</kbd>, um visuelle Betonung wie Unterstreichungen hinzuzufügen.",
|
||||||
|
"task": "Erstelle eine CSS-Regel, die alle Anker-Elemente (<kbd>a</kbd>) mit <kbd>href</kbd>-Attributen, die mit <kbd>\"https\"</kbd> beginnen, anvisiert. Style sie mit <kbd>green</kbd>-Textfarbe und <kbd>underline</kbd>-Textdekoration.",
|
||||||
|
"previewHTML": "<h2>Verschiedene Arten von Links</h2>\n<ul>\n <li><a href=\"https://example.com\">Externer HTTPS-Link</a></li>\n <li><a href=\"http://oldsite.com\">Externer HTTP-Link</a></li>\n <li><a href=\"#section1\">Interner Anker-Link</a></li>\n <li><a href=\"/about\">Relativer Link</a></li>\n <li><a href=\"https://secure-site.org\">Ein weiterer HTTPS-Link</a></li>\n</ul>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; } ul { list-style-type: none; padding: 0; } li { margin-bottom: 8px; } a { text-decoration: none; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Visiere Anker-Elemente an, deren href mit \"https\" beginnt */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"solution": "a[href^=\"https\"] {\n color: green;\n text-decoration: underline;\n}",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^a\\[href\\^=\"https\"\\]\\s*{",
|
||||||
|
"message": "Verwende <kbd>a[href^=\"https\"] { … }</kbd> als deinen Attribut-Selektor, um HTTPS-Links anzuvisieren",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "color:",
|
||||||
|
"message": "Füge die <kbd>color</kbd>-Eigenschaft hinzu, um die Textfarbe zu setzen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "color",
|
||||||
|
"expected": "green"
|
||||||
|
},
|
||||||
|
"message": "Setze die Textfarbe auf <kbd>green</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "text-decoration:",
|
||||||
|
"message": "Füge die <kbd>text-decoration</kbd>-Eigenschaft hinzu, um das Link-Erscheinungsbild zu stylen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "text-decoration",
|
||||||
|
"expected": "underline"
|
||||||
|
},
|
||||||
|
"message": "Setze text-decoration auf <kbd>underline</kbd>, um HTTPS-Links zu unterstreichen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "a\\[href\\^=\"https\"\\]\\s*{[^}]*}",
|
||||||
|
"message": "Vergiss nicht, deine CSS-Regel mit einer schließenden Klammer <kbd>}</kbd> zu beenden",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "child-combinator",
|
||||||
|
"title": "Kind-Kombinator: Nur direkte Kinder",
|
||||||
|
"description": "Der Kind-Kombinator (<kbd>></kbd>) wählt Elemente aus, die direkte Kinder eines anderen Elements sind, keine Enkel oder tiefere Nachfahren. Dies ist entscheidend, wenn du verschachtelte Strukturen hast, bei denen du nur die äußere Ebene stylen möchtest. Zum Beispiel könntest du in einem Navigationsmenü mit Dropdowns möchten, dass Hauptmenüpunkte anders gestylt werden als Untermenüpunkte. Der Kind-Kombinator (<kbd>></kbd>) gibt dir chirurgische Präzision – <kbd>ul > li</kbd> visiert nur direkte Listenelemente an, während <kbd>ul li</kbd> ALLE Listenelemente einschließlich verschachtelter anvisieren würde. Dies verhindert Stil-Vererbungschaos in komplexen Layouts.",
|
||||||
|
"task": "Verwende den Kind-Kombinator, um nur die direkten <kbd>li</kbd>-Kinder von <kbd>.main-nav</kbd> anzuvisieren. Gib ihnen einen <kbd>cornflowerblue</kbd>-Hintergrund und <kbd>white</kbd>-Textfarbe. Beachte, wie die verschachtelten Untermenü-Elemente völlig ungestylt bleiben!",
|
||||||
|
"previewHTML": "<ul class=\"main-nav\">\n <li>🏠 Startseite</li>\n <li>📱 Produkte\n <ul>\n <li>💻 Laptops</li>\n <li>📱 Handys</li>\n <li>⌚ Uhren</li>\n </ul>\n </li>\n <li>ℹ️ Über uns\n <ul>\n <li>👥 Team</li>\n <li>📍 Standort</li>\n </ul>\n </li>\n <li>📧 Kontakt</li>\n</ul>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 20px; background: #f5f5f5; } .main-nav { background: white; border-radius: 8px; padding: 0; margin: 0; box-shadow: 0 2px 8px rgba(0,0,0,0.1); list-style: none; } .main-nav li { padding: 12px 16px; margin: 2px 0; cursor: pointer; transition: all 0.2s; } .main-nav ul { margin: 8px 0 0 20px; padding: 0; list-style: none; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Visiere nur die direkten li-Kinder von .main-nav an (nicht verschachtelte Untermenü-Elemente) */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"solution": ".main-nav > li {\n background-color: cornflowerblue;\n color: white;\n}",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^\\.main-nav\\s*>\\s*li\\s*{",
|
||||||
|
"message": "Verwende <kbd>.main-nav > li { … }</kbd> mit dem Kind-Kombinator, um nur direkte Kinder anzuvisieren",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "background-color:",
|
||||||
|
"message": "Füge die <kbd>background-color</kbd>-Eigenschaft hinzu, um Hauptmenüpunkte hervorzuheben"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "background-color",
|
||||||
|
"expected": "cornflowerblue"
|
||||||
|
},
|
||||||
|
"message": "Setze background-color auf <kbd>cornflowerblue</kbd> für das Hauptmenü-Styling"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "color:",
|
||||||
|
"message": "Füge die <kbd>color</kbd>-Eigenschaft hinzu, um die Textfarbe zu setzen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "color",
|
||||||
|
"expected": "white"
|
||||||
|
},
|
||||||
|
"message": "Setze die Textfarbe auf <kbd>white</kbd> für Kontrast gegen den blauen Hintergrund"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "\\.main-nav\\s*>\\s*li\\s*{[^}]*}",
|
||||||
|
"message": "Vergiss nicht, deine CSS-Regel mit einer schließenden Klammer <kbd>}</kbd> zu beenden",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "descendant-combinator",
|
||||||
|
"title": "Nachfahren-Kombinator: Alle verschachtelten Elemente",
|
||||||
|
"description": "Der Nachfahren-Kombinator verwendet ein Leerzeichen zwischen Selektoren, um Elemente anzuvisieren, die irgendwo in anderen Elementen verschachtelt sind, unabhängig davon, wie tief verschachtelt sie sind. Zum Beispiel wählt <kbd>nav a</kbd> alle Anker-Elemente innerhalb von Navigations-Elementen aus, ob sie direkte Kinder oder mehrere Ebenen tief verschachtelt sind. Dies ist breiter als der Kind-Kombinator und nützlich für das Anwenden einheitlichen Stylings auf alle Elemente eines bestimmten Typs innerhalb eines Containers. Beim Stylen von Nachfahren kannst du Eigenschaften wie <kbd>text-decoration</kbd> verwenden, um das Link-Erscheinungsbild zu kontrollieren, und <kbd>color</kbd>, um einheitliche Textfarben in einem Abschnitt zu setzen. Der Nachfahren-Kombinator ist einer der am häufigsten verwendeten Kombinatoren in CSS.",
|
||||||
|
"task": "Verwende den Nachfahren-Kombinator, um alle Anker-Elemente (<kbd>a</kbd>) innerhalb des <kbd>nav</kbd>-Elements anzuvisieren. Entferne ihre Unterstreichungen mit <kbd>text-decoration: none</kbd> und mache sie <kbd>blue</kbd>.",
|
||||||
|
"previewHTML": "<nav>\n <ul>\n <li><a href=\"#\">Startseite</a></li>\n <li><a href=\"#\">Über uns</a>\n <ul>\n <li><a href=\"#\">Unser Team</a></li>\n <li><a href=\"#\">Geschichte</a></li>\n </ul>\n </li>\n <li><a href=\"#\">Kontakt</a></li>\n </ul>\n</nav>\n<p>Dieser <a href=\"#\">Link außerhalb von nav</a> sollte nicht betroffen sein.</p>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; } nav { border: 2px solid navy; padding: 15px; background-color: aliceblue; margin-bottom: 15px; } ul { list-style-type: none; padding-left: 20px; } li { margin-bottom: 5px; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Visiere alle Anker-Elemente innerhalb von nav mit dem Nachfahren-Kombinator an */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"solution": "nav a {\n text-decoration: none;\n color: blue;\n}",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^nav\\s+a\\s*{",
|
||||||
|
"message": "Verwende <kbd>nav a</kbd> mit einem Leerzeichen zwischen nav und a",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "text-decoration:",
|
||||||
|
"message": "Füge die <kbd>text-decoration</kbd>-Eigenschaft hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "text-decoration",
|
||||||
|
"expected": "none"
|
||||||
|
},
|
||||||
|
"message": "Setze text-decoration auf <kbd>none</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "color:",
|
||||||
|
"message": "Füge die <kbd>color</kbd>-Eigenschaft hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "color",
|
||||||
|
"expected": "blue"
|
||||||
|
},
|
||||||
|
"message": "Setze color auf <kbd>blue</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "nav\\s+a\\s*{[^}]*}",
|
||||||
|
"message": "Vergiss nicht, deine CSS-Regel mit einer schließenden Klammer <kbd>}</kbd> zu beenden",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "adjacent-sibling-combinator",
|
||||||
|
"title": "Benachbarter-Geschwister-Kombinator: Das nächste Element",
|
||||||
|
"description": "Der benachbarte-Geschwister-Kombinator (<kbd>+</kbd>) wählt ein Element aus, das einem anderen Element auf derselben Ebene in der HTML-Struktur unmittelbar folgt. Beide Elemente müssen denselben Elternteil haben, und es dürfen keine anderen Elemente zwischen ihnen sein. Zum Beispiel wählt <kbd>h1 + p</kbd> Absatz-Elemente aus, die direkt nach h1-Überschriften kommen. Dieser Kombinator ist besonders nützlich für das Styling von Elementen, die besondere Beziehungen haben, wie der erste Absatz nach einer Überschrift oder Labels, die Formulareingaben folgen. Beim Stylen benachbarter Geschwister kannst du Eigenschaften wie <kbd>margin-top</kbd> verwenden, um Abstände anzupassen, und <kbd>font-style</kbd>, um Betonung wie Kursivschrift hinzuzufügen, um visuelle Hierarchie zu schaffen.",
|
||||||
|
"task": "Verwende den benachbarten-Geschwister-Kombinator, um Absätze anzuvisieren, die unmittelbar auf <kbd>h2</kbd>-Überschriften folgen. Entferne ihren oberen Rand mit <kbd>margin-top: 0</kbd> und mache sie kursiv.",
|
||||||
|
"previewHTML": "<h2>Erste Überschrift</h2>\n<p>Dieser Absatz folgt direkt auf h2 (sollte betroffen sein).</p>\n<p>Dieser Absatz kommt nach einem anderen Absatz (sollte NICHT betroffen sein).</p>\n<h2>Zweite Überschrift</h2>\n<p>Dieser Absatz folgt auch direkt auf h2 (sollte betroffen sein).</p>\n<div>Dieses div kommt nach h2, ist aber kein Absatz.</div>\n<p>Dieser Absatz kommt nach einem div (sollte NICHT betroffen sein).</p>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; } h2, p, div { margin: 15px 0; padding: 8px; border: 1px dashed #ccc; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Visiere Absätze an, die unmittelbar auf h2-Überschriften folgen */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"solution": "h2 + p {\n margin-top: 0;\n font-style: italic;\n}",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^h2\\s*\\+\\s*p\\s*{",
|
||||||
|
"message": "Verwende <kbd>h2 + p</kbd> mit dem benachbarten-Geschwister-Kombinator (+)",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "margin-top:",
|
||||||
|
"message": "Füge die <kbd>margin-top</kbd>-Eigenschaft hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "margin-top",
|
||||||
|
"expected": "0"
|
||||||
|
},
|
||||||
|
"message": "Setze margin-top auf <kbd>0</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "font-style:",
|
||||||
|
"message": "Füge die <kbd>font-style</kbd>-Eigenschaft hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "font-style",
|
||||||
|
"expected": "italic"
|
||||||
|
},
|
||||||
|
"message": "Setze font-style auf <kbd>italic</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "h2\\s*\\+\\s*p\\s*{[^}]*}",
|
||||||
|
"message": "Vergiss nicht, deine CSS-Regel mit einer schließenden Klammer <kbd>}</kbd> zu beenden",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "general-sibling-combinator",
|
||||||
|
"title": "Allgemeiner-Geschwister-Kombinator: Alle folgenden Geschwister",
|
||||||
|
"description": "Der allgemeine-Geschwister-Kombinator (<kbd>~</kbd>) wählt alle Elemente aus, die einem anderen Element auf derselben Ebene folgen, nicht nur das unmittelbar benachbarte. Im Gegensatz zum benachbarten-Geschwister-Kombinator können andere Elemente zwischen den Zielelementen sein, solange sie alle denselben Elternteil haben und die ausgewählten Elemente nach dem Referenzelement kommen. Zum Beispiel wählt <kbd>h2 ~ p</kbd> alle Absatz-Elemente aus, die nach einer h2-Überschrift auf derselben Ebene erscheinen. Beim Stylen allgemeiner Geschwister kannst du Eigenschaften wie <kbd>color</kbd> verwenden, um die Textfarbe zu ändern, und <kbd>padding-left</kbd>, um visuelle Einrückung zu schaffen, was hilft, die Beziehung zwischen verwandten Inhaltsabschnitten zu zeigen.",
|
||||||
|
"task": "Verwende den allgemeinen-Geschwister-Kombinator, um alle Absätze anzuvisieren, die nach der <kbd>h3</kbd>-Überschrift kommen (auf derselben Ebene). Gib ihnen eine <kbd>gray</kbd>-Farbe und <kbd>20px</kbd> linkes Padding.",
|
||||||
|
"previewHTML": "<div>\n <p>Dieser Absatz kommt vor h3 (sollte NICHT betroffen sein).</p>\n <h3>Abschnittstitel</h3>\n <p>Erster Absatz nach h3 (sollte betroffen sein).</p>\n <div>Etwas anderer Inhalt dazwischen</div>\n <p>Zweiter Absatz nach h3 (sollte auch betroffen sein).</p>\n <span>Mehr Inhalt</span>\n <p>Dritter Absatz nach h3 (sollte auch betroffen sein).</p>\n</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; } h3, p, div, span { margin: 10px 0; padding: 8px; border: 1px dashed #ccc; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Visiere alle Absätze an, die h3 folgen, mit dem allgemeinen-Geschwister-Kombinator */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"solution": "h3 ~ p {\n color: gray;\n padding-left: 20px;\n}",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^h3\\s*~\\s*p\\s*{",
|
||||||
|
"message": "Verwende <kbd>h3 ~ p</kbd> mit dem allgemeinen-Geschwister-Kombinator (~)",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "color:",
|
||||||
|
"message": "Füge die <kbd>color</kbd>-Eigenschaft hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "color",
|
||||||
|
"expected": "gray"
|
||||||
|
},
|
||||||
|
"message": "Setze color auf <kbd>gray</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "padding-left:",
|
||||||
|
"message": "Füge die <kbd>padding-left</kbd>-Eigenschaft hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "padding-left",
|
||||||
|
"expected": "20px"
|
||||||
|
},
|
||||||
|
"message": "Setze padding-left auf <kbd>20px</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "h3\\s*~\\s*p\\s*{[^}]*}",
|
||||||
|
"message": "Vergiss nicht, deine CSS-Regel mit einer schließenden Klammer <kbd>}</kbd> zu beenden",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "hover-pseudo-class",
|
||||||
|
"title": "Die :hover Pseudo-Klasse",
|
||||||
|
"description": "Die <kbd>:hover</kbd> Pseudo-Klasse wendet Stile an, wenn ein Benutzer mit der Maus über ein Element fährt. Dies schafft interaktives Feedback, das die Benutzererfahrung verbessert, indem es visuelle Hinweise über klickbare oder interaktive Elemente gibt. Hover-Effekte werden häufig bei Links, Buttons und anderen interaktiven Elementen verwendet, um ihre interaktive Natur anzuzeigen. Beim Erstellen von Hover-Effekten kannst du Eigenschaften wie <kbd>background-color</kbd> verwenden, um den Hintergrund beim Hover zu ändern, und <kbd>color</kbd>, um die Textfarbe zu ändern, was klares visuelles Feedback schafft. Die <kbd>:hover</kbd> Pseudo-Klasse gilt nur, während der Mauszeiger über dem Element positioniert ist, und kehrt zum normalen Zustand zurück, wenn der Zeiger sich entfernt.",
|
||||||
|
"task": "Erstelle einen Hover-Effekt für das Button-Element. Beim Hover ändere den Hintergrund auf <kbd>darkblue</kbd> und die Textfarbe auf <kbd>white</kbd>.",
|
||||||
|
"previewHTML": "<h2>Interaktiver Button</h2>\n<p>Fahre mit der Maus über den Button unten, um den Effekt zu sehen:</p>\n<button type=\"button\">Fahre über mich</button>\n<p>Der Button sollte seine Farben ändern, wenn du mit der Maus darüber fährst.</p>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; } button { padding: 12px 24px; font-size: 16px; border: 2px solid darkblue; background-color: lightblue; color: darkblue; border-radius: 5px; cursor: pointer; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Erstelle einen Hover-Effekt für den Button */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"solution": "button:hover {\n background-color: darkblue;\n color: white;\n}",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^button:hover\\s*{",
|
||||||
|
"message": "Verwende <kbd>button:hover</kbd>, um Buttons beim Hover anzuvisieren",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "background-color:",
|
||||||
|
"message": "Füge die <kbd>background-color</kbd>-Eigenschaft hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "background-color",
|
||||||
|
"expected": "darkblue"
|
||||||
|
},
|
||||||
|
"message": "Setze background-color auf <kbd>darkblue</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "color:",
|
||||||
|
"message": "Füge die <kbd>color</kbd>-Eigenschaft hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "color",
|
||||||
|
"expected": "white"
|
||||||
|
},
|
||||||
|
"message": "Setze color auf <kbd>white</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "button:hover\\s*{[^}]*}",
|
||||||
|
"message": "Vergiss nicht, deine CSS-Regel mit einer schließenden Klammer <kbd>}</kbd> zu beenden",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "first-child-pseudo-class",
|
||||||
|
"title": "Die :first-child Pseudo-Klasse",
|
||||||
|
"description": "Die <kbd>:first-child</kbd> Pseudo-Klasse wählt Elemente aus, die das erste Kind ihres Elternelements sind. Dies ist nützlich für das Anwenden spezieller Stile auf das erste Element in Listen, den ersten Absatz in Artikeln oder das erste Element in einem beliebigen Container. Zum Beispiel wählt <kbd>li:first-child</kbd> das erste Listenelement in jeder Liste aus, während <kbd>p:first-child</kbd> Absätze auswählt, die das erste Kind-Element ihres Containers sind. Beim Stylen erster Kinder kannst du Eigenschaften wie <kbd>font-weight</kbd> verwenden, um das erste Element fett zu machen, und <kbd>margin-top</kbd>, um Abstände anzupassen, was hilft, visuelle Hierarchie zu schaffen und das Layout deines Inhalts zu verbessern.",
|
||||||
|
"task": "Verwende die <kbd>:first-child</kbd> Pseudo-Klasse, um das erste Listenelement in jeder Liste anzuvisieren. Mache es <kbd>bold</kbd> und entferne seinen oberen Rand.",
|
||||||
|
"previewHTML": "<h2>Mehrere Listen</h2>\n<h3>Früchte</h3>\n<ul>\n <li>Apfel (erstes Kind)</li>\n <li>Banane</li>\n <li>Orange</li>\n</ul>\n<h3>Farben</h3>\n<ul>\n <li>Rot (erstes Kind)</li>\n <li>Blau</li>\n <li>Grün</li>\n</ul>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; } li { margin: 8px 0; padding: 5px; border: 1px solid #ddd; border-radius: 3px; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Visiere das erste Listenelement in jeder Liste an */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"solution": "li:first-child {\n font-weight: bold;\n margin-top: 0;\n}",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^li:first-child\\s*{",
|
||||||
|
"message": "Verwende <kbd>li:first-child</kbd>, um erste Listenelemente anzuvisieren",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "font-weight:",
|
||||||
|
"message": "Füge die <kbd>font-weight</kbd>-Eigenschaft hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "font-weight",
|
||||||
|
"expected": "bold"
|
||||||
|
},
|
||||||
|
"message": "Setze font-weight auf <kbd>bold</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "margin-top:",
|
||||||
|
"message": "Füge die <kbd>margin-top</kbd>-Eigenschaft hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "margin-top",
|
||||||
|
"expected": "0"
|
||||||
|
},
|
||||||
|
"message": "Setze margin-top auf <kbd>0</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "li:first-child\\s*{[^}]*}",
|
||||||
|
"message": "Vergiss nicht, deine CSS-Regel mit einer schließenden Klammer <kbd>}</kbd> zu beenden",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
160
lessons/de/10-tailwind-basics.json
Normal file
160
lessons/de/10-tailwind-basics.json
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "tailwind-basics",
|
||||||
|
"title": "Tailwind: Grundlagen",
|
||||||
|
"description": "Lerne, wie Tailwind CSS das Styling revolutioniert, indem es traditionelle CSS-Selektoren durch Utility-First-Klassen ersetzt. Verstehe die Philosophie hinter Utility-Klassen und wie sie häufige CSS-Probleme wie Spezifitätskonflikte und Wartungskomplexität lösen.",
|
||||||
|
"mode": "tailwind",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "bg-colors",
|
||||||
|
"title": "Hintergrundfarben",
|
||||||
|
"description": "Lerne, Hintergrundfarben mit Tailwind-Utilities anzuwenden.",
|
||||||
|
"task": "Füge dem div einen blauen Hintergrund mit Tailwind-Klassen hinzu.",
|
||||||
|
"previewHTML": "<div class=\"{{USER_CLASSES}} p-8 rounded\">Hallo Tailwind!</div>",
|
||||||
|
"previewBaseCSS": "body { padding: 20px; font-family: sans-serif; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "contains_class",
|
||||||
|
"value": "bg-blue-500",
|
||||||
|
"message": "Füge die 'bg-blue-500'-Klasse für einen blauen Hintergrund hinzu."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "utility-first-philosophy",
|
||||||
|
"title": "Den Utility-First-Ansatz verstehen",
|
||||||
|
"description": "Tailwind CSS folgt einem Utility-First-Ansatz, bei dem du anstelle von benutzerdefinierten CSS-Klassen Designs mit kleinen, einzweckigen Utility-Klassen zusammenstellst. Im Gegensatz zu traditionellem CSS, wo du möglicherweise <kbd>.card { background: white; padding: 1rem; border-radius: 0.5rem; }</kbd> schreibst, bietet Tailwind vorgefertigte Utilities wie <kbd>bg-white</kbd>, <kbd>p-4</kbd> und <kbd>rounded</kbd>.<br><br>Das <kbd>bg-white</kbd>-Utility setzt <kbd>background-color: white</kbd>, <kbd>p-4</kbd> wendet <kbd>padding: 1rem</kbd> auf allen Seiten an, und <kbd>rounded</kbd> fügt <kbd>border-radius: 0.25rem</kbd> hinzu. Dieser Ansatz eliminiert die Notwendigkeit, zwischen HTML- und CSS-Dateien zu wechseln.",
|
||||||
|
"task": "Erstelle einen weißen kartenähnlichen Container mit einem kleinen Schlagschatten, 1rem Padding und abgerundeten Ecken.",
|
||||||
|
"previewHTML": "<div class=\"bg-gray-100 h-72 p-6\">\n <div class=\"{{USER_CLASSES}}\">\n <h3 class=\"text-lg font-semibold mb-2\">Kartentitel</h3>\n <p class=\"text-gray-600\">Dies ist eine Karten-Komponente, die vollständig mit Utility-Klassen erstellt wurde!</p>\n </div>\n</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; margin: 0; }",
|
||||||
|
"sandboxCSS": "/* Traditioneller CSS-Ansatz:\n.card {\n background-color: white;\n padding: 1rem;\n border-radius: 0.25rem;\n}\n*/",
|
||||||
|
"initialCode": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "contains_class",
|
||||||
|
"value": "bg-white",
|
||||||
|
"message": "Füge <kbd>bg-white</kbd> hinzu, um die Hintergrundfarbe auf weiß zu setzen."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains_class",
|
||||||
|
"value": "p-4",
|
||||||
|
"message": "Füge <kbd>p-4</kbd> hinzu, um 1rem Padding auf allen Seiten anzuwenden."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains_class",
|
||||||
|
"value": "rounded",
|
||||||
|
"message": "Füge <kbd>rounded</kbd> hinzu, um einen border-radius von 0.25rem anzuwenden."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains_class",
|
||||||
|
"value": "shadow-sm",
|
||||||
|
"message": "Füge <kbd>shadow-sm</kbd> hinzu, um einen kleinen Schlagschatten anzuwenden."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "text-utilities",
|
||||||
|
"title": "Textfarbe und Größen-Utilities",
|
||||||
|
"description": "Tailwind bietet umfassende Text-Utilities für Typografie-Styling. Textfarben verwenden das Muster <kbd>text-{farbe}-{abstufung}</kbd>, wobei Farben rot, blau, grün usw. umfassen und Abstufungen von 50 (hellste) bis 950 (dunkelste) reichen. Zum Beispiel wendet <kbd>text-blue-600</kbd> eine mittlere Blaufarbe an.<br><br>Textgrößen folgen dem Muster <kbd>text-{größe}</kbd> mit Optionen wie <kbd>text-sm</kbd> (0.875rem), <kbd>text-base</kbd> (1rem), <kbd>text-lg</kbd> (1.125rem), <kbd>text-xl</kbd> (1.25rem) und größeren Größen bis zu <kbd>text-9xl</kbd>. Schriftgewichte verwenden <kbd>font-{gewicht}</kbd> wie <kbd>font-normal</kbd>, <kbd>font-medium</kbd>, <kbd>font-semibold</kbd> und <kbd>font-bold</kbd>.",
|
||||||
|
"task": "Style die Überschrift mit <kbd>text-blue-600</kbd> für die Farbe, <kbd>text-2xl</kbd> für die Größe und <kbd>font-bold</kbd> für das Gewicht.",
|
||||||
|
"previewHTML": "<div class=\"p-6 bg-gray-50\">\n <h1 class=\"{{USER_CLASSES}} mb-4\">Willkommen bei Tailwind CSS</h1>\n <p class=\"text-gray-700\">Diese Überschrift demonstriert Text-Utilities in Aktion.</p>\n</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; margin: 0; }",
|
||||||
|
"sandboxCSS": "/* Traditionelles CSS wäre:\nh1 {\n color: #2563eb;\n font-size: 1.5rem;\n font-weight: 700;\n}\n*/",
|
||||||
|
"initialCode": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "contains_class",
|
||||||
|
"value": "text-blue-600",
|
||||||
|
"message": "Füge <kbd>text-blue-600</kbd> hinzu, um den Text blau zu machen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains_class",
|
||||||
|
"value": "text-2xl",
|
||||||
|
"message": "Füge <kbd>text-2xl</kbd> hinzu, um die Schriftgröße auf 1.5rem zu erhöhen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains_class",
|
||||||
|
"value": "font-bold",
|
||||||
|
"message": "Füge <kbd>font-bold</kbd> hinzu, um den Text fett zu machen (font-weight: 700)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "spacing-utilities",
|
||||||
|
"title": "Abstände mit Padding und Margin",
|
||||||
|
"description": "Tailwinds Abstands-Utilities folgen einem konsistenten Muster mit einer Abstandsskala, bei der jede Einheit 0.25rem (4px) darstellt. Padding-Utilities verwenden <kbd>p-{größe}</kbd> für alle Seiten, <kbd>px-{größe}</kbd> für horizontal (links/rechts), <kbd>py-{größe}</kbd> für vertikal (oben/unten), oder einzelne Seiten wie <kbd>pt-{größe}</kbd>, <kbd>pr-{größe}</kbd>, <kbd>pb-{größe}</kbd>, <kbd>pl-{größe}</kbd>.<br><br>Häufige Größen sind <kbd>p-2</kbd> (0.5rem), <kbd>p-4</kbd> (1rem), <kbd>p-6</kbd> (1.5rem) und <kbd>p-8</kbd> (2rem). Margin folgt demselben Muster, verwendet aber <kbd>m-</kbd> statt <kbd>p-</kbd>. Zum Beispiel zentriert <kbd>mx-auto</kbd> ein Element horizontal durch automatische linke und rechte Margins.",
|
||||||
|
"task": "Style den Button mit <kbd>px-6</kbd> für horizontales Padding, <kbd>py-3</kbd> für vertikales Padding und <kbd>mx-auto</kbd>, um ihn zu zentrieren.",
|
||||||
|
"previewHTML": "<div class=\"p-6 bg-gray-100\">\n <button class=\"{{USER_CLASSES}} bg-blue-500 text-white rounded block\">\n Zentrierter Button\n </button>\n</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; margin: 0; }",
|
||||||
|
"sandboxCSS": "/* Traditionelles CSS-Äquivalent:\nbutton {\n padding-left: 1.5rem;\n padding-right: 1.5rem;\n padding-top: 0.75rem;\n padding-bottom: 0.75rem;\n margin-left: auto;\n margin-right: auto;\n}\n*/",
|
||||||
|
"initialCode": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "contains_class",
|
||||||
|
"value": "px-6",
|
||||||
|
"message": "Füge <kbd>px-6</kbd> für horizontales Padding hinzu (1.5rem links und rechts)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains_class",
|
||||||
|
"value": "py-3",
|
||||||
|
"message": "Füge <kbd>py-3</kbd> für vertikales Padding hinzu (0.75rem oben und unten)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains_class",
|
||||||
|
"value": "mx-auto",
|
||||||
|
"message": "Füge <kbd>mx-auto</kbd> hinzu, um den Button horizontal zu zentrieren"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "responsive-design",
|
||||||
|
"title": "Responsives Design mit Breakpoint-Präfixen",
|
||||||
|
"description": "Tailwind verwendet einen Mobile-First-Ansatz für responsives Design mit Breakpoint-Präfixen. Die Basis-Utilities gelten für alle Bildschirmgrößen, dann fügst du Präfixe für größere Bildschirme hinzu: <kbd>sm:</kbd> (640px+), <kbd>md:</kbd> (768px+), <kbd>lg:</kbd> (1024px+), <kbd>xl:</kbd> (1280px+) und <kbd>2xl:</kbd> (1536px+).<br><br>Zum Beispiel macht <kbd>text-base md:text-lg lg:text-xl</kbd> Text auf Mobil normal groß, größer auf Tablets (md) und noch größer auf Desktop (lg). Jeder Breakpoint überschreibt den vorherigen, also bedeutet <kbd>p-4 md:p-6 lg:p-8</kbd> 1rem Padding auf Mobil, 1.5rem auf Tablets und 2rem auf Desktop.<br><br>Breiten-Utilities wie <kbd>w-full</kbd> (100% Breite), <kbd>w-1/2</kbd> (50% Breite) oder feste Größen wie <kbd>w-64</kbd> (16rem) können auch responsiv gemacht werden.",
|
||||||
|
"task": "Mache die Box responsiv: <kbd>w-full</kbd> auf Mobil, <kbd>md:w-1/2</kbd> auf Tablets und <kbd>lg:w-1/3</kbd> auf Desktop. Füge auch responsive Textgrößen hinzu mit <kbd>text-lg</kbd>, <kbd>md:text-xl</kbd> und <kbd>lg:text-2xl</kbd>.",
|
||||||
|
"previewHTML": "<div class=\"p-6 bg-gray-100\">\n <div class=\"{{USER_CLASSES}} bg-purple-500 text-white p-6 rounded text-center\">\n <span>Responsive Box</span><br>\n <small class=\"opacity-75\">Ändere die Browsergröße, um den Effekt zu sehen!</small>\n </div>\n</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; margin: 0; }",
|
||||||
|
"sandboxCSS": "/* Traditionelles CSS würde Media Queries erfordern:\n.responsive-box {\n width: 100%;\n font-size: 1.125rem;\n}\n@media (min-width: 768px) {\n .responsive-box {\n width: 50%;\n font-size: 1.25rem;\n }\n}\n@media (min-width: 1024px) {\n .responsive-box {\n width: 33.333333%;\n font-size: 1.5rem;\n }\n}\n*/",
|
||||||
|
"initialCode": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "contains_class",
|
||||||
|
"value": "w-full",
|
||||||
|
"message": "Füge <kbd>w-full</kbd> für 100% Breite auf Mobil hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains_class",
|
||||||
|
"value": "md:w-1/2",
|
||||||
|
"message": "Füge <kbd>md:w-1/2</kbd> für 50% Breite auf Tablet und größer hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains_class",
|
||||||
|
"value": "lg:w-1/3",
|
||||||
|
"message": "Füge <kbd>lg:w-1/3</kbd> für 33.33% Breite auf Desktop und größer hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains_class",
|
||||||
|
"value": "text-lg",
|
||||||
|
"message": "Füge <kbd>text-lg</kbd> für die Basis-Textgröße hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains_class",
|
||||||
|
"value": "md:text-xl",
|
||||||
|
"message": "Füge <kbd>md:text-xl</kbd> für größeren Text auf Tablets hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains_class",
|
||||||
|
"value": "lg:text-2xl",
|
||||||
|
"message": "Füge <kbd>lg:text-2xl</kbd> für noch größeren Text auf Desktop hinzu"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
97
lessons/de/20-html-elements.json
Normal file
97
lessons/de/20-html-elements.json
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "html-elements",
|
||||||
|
"title": "HTML-Elemente: Block vs Inline",
|
||||||
|
"description": "Verstehe den grundlegenden Unterschied zwischen Container- (Block-) und Inline-Elementen",
|
||||||
|
"mode": "html",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "block-vs-inline-intro",
|
||||||
|
"title": "Block- vs Inline-Elemente",
|
||||||
|
"description": "HTML-Elemente fallen in zwei Hauptkategorien:<br><br><strong>Block-Elemente</strong> (Container) beginnen in einer neuen Zeile und nehmen die volle Breite ein. Beispiele: <kbd><div></kbd>, <kbd><p></kbd>, <kbd><h1></kbd>, <kbd><section></kbd><br><br><strong>Inline-Elemente</strong> fließen innerhalb des Textes und nehmen nur die benötigte Breite ein. Beispiele: <kbd><span></kbd>, <kbd><a></kbd>, <kbd><strong></kbd>, <kbd><em></kbd>",
|
||||||
|
"task": "Umschließe das Wort <kbd>wichtig</kbd> mit <kbd><strong></kbd>-Tags, um es fett zu machen. Beachte, wie der Absatz (Block) die volle Breite einnimmt, während strong (Inline) mit dem Text fließt.",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 20px; } p { background: #e3f2fd; padding: 10px; } strong { background: #ffecb3; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "<p>Dies ist ein Absatz mit einem wichtig Wort.</p>",
|
||||||
|
"solution": "<p>Dies ist ein Absatz mit einem <strong>wichtig</strong> Wort.</p>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "p",
|
||||||
|
"message": "Füge ein <p> Absatz-Element hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "parent_child",
|
||||||
|
"value": { "parent": "p", "child": "strong" },
|
||||||
|
"message": "Umschließe das Wort 'wichtig' mit <strong>-Tags"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "semantic-containers",
|
||||||
|
"title": "Semantische Container-Elemente",
|
||||||
|
"description": "Modernes HTML verwendet semantische Container, die ihren Inhalt beschreiben:<br><br><kbd><header></kbd> - Kopfbereich der Seite oder eines Abschnitts<br><kbd><nav></kbd> - Navigationslinks<br><kbd><main></kbd> - Hauptinhalt<br><kbd><section></kbd> - Thematische Gruppierung<br><kbd><article></kbd> - Eigenständiger Inhalt<br><kbd><footer></kbd> - Fußbereich der Seite oder eines Abschnitts",
|
||||||
|
"task": "Erstelle eine einfache Seitenstruktur:<br>1. Füge ein <kbd><header></kbd> mit einem <kbd><h1></kbd> hinzu, das den Text 'Meine Webseite' enthält<br>2. Füge ein <kbd><main></kbd>-Element mit einem Absatz hinzu, der 'Willkommen auf meiner Seite!' sagt<br>3. Füge ein <kbd><footer></kbd> mit einem Absatz hinzu, der 'Copyright 2025' sagt",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; margin: 0; } header { background: #1976d2; color: white; padding: 15px; } main { padding: 20px; min-height: 100px; } footer { background: #424242; color: white; padding: 10px; text-align: center; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "<!-- Erstelle hier deine Seitenstruktur -->",
|
||||||
|
"solution": "<header>\n <h1>Meine Webseite</h1>\n</header>\n<main>\n <p>Willkommen auf meiner Seite!</p>\n</main>\n<footer>\n <p>Copyright 2025</p>\n</footer>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "header",
|
||||||
|
"message": "Füge ein <header>-Element hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "main",
|
||||||
|
"message": "Füge ein <main>-Element hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "footer",
|
||||||
|
"message": "Füge ein <footer>-Element hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "parent_child",
|
||||||
|
"value": { "parent": "header", "child": "h1" },
|
||||||
|
"message": "Füge eine <h1>-Überschrift in deinem Header hinzu"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "div-vs-span",
|
||||||
|
"title": "Generische Container: div und span",
|
||||||
|
"description": "Wenn du einen Container ohne semantische Bedeutung benötigst:<br><br><kbd><div></kbd> - Generischer Block-Container (für Layout/Gruppierung)<br><kbd><span></kbd> - Generischer Inline-Container (zum Stylen von Textteilen)<br><br>Verwende semantische Elemente wenn möglich, div/span wenn kein semantisches Element passt.",
|
||||||
|
"task": "Umschließe das Wort 'hervorgehoben' mit einem <kbd><span></kbd>, um es anders zu gestalten. Umschließe das gesamte Zitat mit einem <kbd><div></kbd>.",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: Georgia, serif; padding: 20px; } div { background: #f5f5f5; padding: 15px; border-left: 4px solid #1976d2; } span { background: #fff59d; padding: 2px 4px; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "Der hervorgehoben Moment war unvergesslich.",
|
||||||
|
"solution": "<div>Der <span>hervorgehoben</span> Moment war unvergesslich.</div>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "div",
|
||||||
|
"message": "Umschließe alles mit einem <div>-Element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "span",
|
||||||
|
"message": "Füge ein <span> um das Wort 'hervorgehoben' hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_text",
|
||||||
|
"value": { "selector": "span", "text": "hervorgehoben" },
|
||||||
|
"message": "Das <span> sollte das Wort 'hervorgehoben' enthalten"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
102
lessons/de/21-html-forms-basic.json
Normal file
102
lessons/de/21-html-forms-basic.json
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "html-forms-basic",
|
||||||
|
"title": "HTML-Formulare: Grundlegende Eingaben",
|
||||||
|
"description": "Lerne, Formulare mit verschiedenen Eingabetypen zu erstellen",
|
||||||
|
"mode": "html",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "form-structure",
|
||||||
|
"title": "Formularstruktur",
|
||||||
|
"description": "Jedes Formular benötigt einen <kbd><form></kbd>-Wrapper. Innerhalb verwendest du <kbd><label></kbd> zur Beschreibung der Eingaben und <kbd><input></kbd> für die Dateneingabe.<br><br>Das <kbd>for</kbd>-Attribut bei Labels sollte mit der <kbd>id</kbd> der Eingaben übereinstimmen für bessere Zugänglichkeit.",
|
||||||
|
"task": "Erstelle ein Formular mit:<br>1. Einem <kbd><label></kbd> mit dem Text 'Name:' und dem <kbd>for=\"name\"</kbd>-Attribut<br>2. Einem Text-<kbd><input></kbd> mit <kbd>id=\"name\"</kbd> und <kbd>name=\"name\"</kbd>-Attributen",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 300px; } label { display: block; margin-bottom: 5px; font-weight: 500; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "<!-- Erstelle hier dein Formular -->",
|
||||||
|
"solution": "<form>\n <label for=\"name\">Name:</label>\n <input type=\"text\" id=\"name\" name=\"name\">\n</form>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "form",
|
||||||
|
"message": "Umschließe alles mit einem <form>-Element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "label",
|
||||||
|
"message": "Füge ein <label> für deine Eingabe hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "input",
|
||||||
|
"message": "Füge ein <input>-Element hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "label", "attr": "for", "value": null },
|
||||||
|
"message": "Füge ein 'for'-Attribut zu deinem Label hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "input", "attr": "id", "value": null },
|
||||||
|
"message": "Füge ein 'id'-Attribut zu deiner Eingabe hinzu"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "input-types",
|
||||||
|
"title": "Eingabetypen",
|
||||||
|
"description": "Verschiedene Eingabetypen bieten passende Tastaturen und Validierung:<br><br><kbd>type=\"text\"</kbd> - Allgemeiner Text<br><kbd>type=\"email\"</kbd> - E-Mail mit @-Validierung<br><kbd>type=\"password\"</kbd> - Versteckte Zeichen<br><kbd>type=\"number\"</kbd> - Numerische Tastatur<br><kbd>type=\"tel\"</kbd> - Telefon-Tastatur",
|
||||||
|
"task": "Erstelle ein Anmeldeformular mit zwei Feldern:<br>1. Ein E-Mail-Feld: <kbd><label for=\"email\">E-Mail:</label></kbd> und <kbd><input type=\"email\" id=\"email\"></kbd><br>2. Ein Passwort-Feld: <kbd><label for=\"password\">Passwort:</label></kbd> und <kbd><input type=\"password\" id=\"password\"></kbd><br><br>Optional kannst du jedes in ein <kbd><div class=\"form-group\"></kbd> für Abstände einschließen.",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 300px; } .form-group { margin-bottom: 15px; } label { display: block; margin-bottom: 5px; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "<form>\n <!-- Füge E-Mail- und Passwort-Eingaben hinzu -->\n</form>",
|
||||||
|
"solution": "<form>\n <div class=\"form-group\">\n <label for=\"email\">E-Mail:</label>\n <input type=\"email\" id=\"email\" name=\"email\">\n </div>\n <div class=\"form-group\">\n <label for=\"password\">Passwort:</label>\n <input type=\"password\" id=\"password\" name=\"password\">\n </div>\n</form>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "input[type='email']",
|
||||||
|
"message": "Füge eine Eingabe mit type=\"email\" hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "input[type='password']",
|
||||||
|
"message": "Füge eine Eingabe mit type=\"password\" hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_count",
|
||||||
|
"value": { "selector": "label", "min": 2 },
|
||||||
|
"message": "Füge Labels für beide Eingaben hinzu"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "submit-button",
|
||||||
|
"title": "Absende-Button",
|
||||||
|
"description": "Formulare benötigen eine Möglichkeit zum Absenden. Verwende:<br><br><kbd><button type=\"submit\"></kbd> - Bevorzugt, flexibler Inhalt<br><kbd><input type=\"submit\"></kbd> - Einfacher Text-Button<br><br>Der Button-Text sollte handlungsorientiert sein (z.B. 'Anmelden', 'Registrieren', 'Senden').",
|
||||||
|
"task": "Füge dem Formular einen Absende-Button mit dem Text 'Anmelden' hinzu.",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 300px; } .form-group { margin-bottom: 15px; } label { display: block; margin-bottom: 5px; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; } button { width: 100%; padding: 10px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; } button:hover { background: #1565c0; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "<form>\n <div class=\"form-group\">\n <label for=\"email\">E-Mail:</label>\n <input type=\"email\" id=\"email\">\n </div>\n <div class=\"form-group\">\n <label for=\"password\">Passwort:</label>\n <input type=\"password\" id=\"password\">\n </div>\n <!-- Füge Absende-Button hinzu -->\n</form>",
|
||||||
|
"solution": "<form>\n <div class=\"form-group\">\n <label for=\"email\">E-Mail:</label>\n <input type=\"email\" id=\"email\">\n </div>\n <div class=\"form-group\">\n <label for=\"password\">Passwort:</label>\n <input type=\"password\" id=\"password\">\n </div>\n <button type=\"submit\">Anmelden</button>\n</form>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "button[type='submit'], input[type='submit']",
|
||||||
|
"message": "Füge einen Absende-Button zu deinem Formular hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_text",
|
||||||
|
"value": { "selector": "button", "text": "Anmelden" },
|
||||||
|
"message": "Der Button sollte 'Anmelden' anzeigen"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
112
lessons/de/22-html-forms-validation.json
Normal file
112
lessons/de/22-html-forms-validation.json
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "html-forms-validation",
|
||||||
|
"title": "HTML-Formulare: Validierung",
|
||||||
|
"description": "Lerne die eingebauten HTML5-Formular-Validierungsattribute kennen",
|
||||||
|
"mode": "html",
|
||||||
|
"difficulty": "intermediate",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "required-fields",
|
||||||
|
"title": "Pflichtfelder",
|
||||||
|
"description": "Das <kbd>required</kbd>-Attribut verhindert das Absenden des Formulars, wenn das Feld leer ist.<br><br>Füge es zu jeder Eingabe hinzu, die ausgefüllt werden muss:<br><kbd><input type=\"text\" required></kbd><br><br>Der Browser zeigt automatisch eine Validierungsmeldung an.",
|
||||||
|
"task": "Mache sowohl das Name- als auch das E-Mail-Feld zu Pflichtfeldern.",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 350px; } .form-group { margin-bottom: 15px; } label { display: block; margin-bottom: 5px; } label .required { color: #d32f2f; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; } input:invalid { border-color: #d32f2f; } button { padding: 10px 20px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "<form>\n <div class=\"form-group\">\n <label for=\"name\">Name: <span class=\"required\">*</span></label>\n <input type=\"text\" id=\"name\" name=\"name\">\n </div>\n <div class=\"form-group\">\n <label for=\"email\">E-Mail: <span class=\"required\">*</span></label>\n <input type=\"email\" id=\"email\" name=\"email\">\n </div>\n <button type=\"submit\">Absenden</button>\n</form>",
|
||||||
|
"solution": "<form>\n <div class=\"form-group\">\n <label for=\"name\">Name: <span class=\"required\">*</span></label>\n <input type=\"text\" id=\"name\" name=\"name\" required>\n </div>\n <div class=\"form-group\">\n <label for=\"email\">E-Mail: <span class=\"required\">*</span></label>\n <input type=\"email\" id=\"email\" name=\"email\" required>\n </div>\n <button type=\"submit\">Absenden</button>\n</form>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "input[name='name']", "attr": "required", "value": true },
|
||||||
|
"message": "Füge das 'required'-Attribut zur Name-Eingabe hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "input[name='email']", "attr": "required", "value": true },
|
||||||
|
"message": "Füge das 'required'-Attribut zur E-Mail-Eingabe hinzu"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "input-constraints",
|
||||||
|
"title": "Eingabebeschränkungen",
|
||||||
|
"description": "Kontrolliere, was Benutzer eingeben können:<br><br><kbd>minlength</kbd> / <kbd>maxlength</kbd> - Textlängenbegrenzung<br><kbd>min</kbd> / <kbd>max</kbd> - Zahlenbereich<br><kbd>pattern</kbd> - Regex-Musterabgleich<br><kbd>placeholder</kbd> - Hinweistext (kein Label!)",
|
||||||
|
"task": "Füge Validierung zur Passwort-Eingabe hinzu:<br>1. Füge <kbd>minlength=\"8\"</kbd> für die Mindestlänge hinzu<br>2. Füge <kbd>maxlength=\"20\"</kbd> für die Maximallänge hinzu<br>3. Füge <kbd>placeholder=\"Passwort eingeben\"</kbd> als Hinweis hinzu",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 350px; } .form-group { margin-bottom: 15px; } label { display: block; margin-bottom: 5px; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; } input:invalid:not(:placeholder-shown) { border-color: #d32f2f; } .hint { font-size: 12px; color: #666; margin-top: 4px; } button { padding: 10px 20px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "<form>\n <div class=\"form-group\">\n <label for=\"password\">Passwort:</label>\n <input type=\"password\" id=\"password\" name=\"password\" required>\n <div class=\"hint\">Muss 8-20 Zeichen lang sein</div>\n </div>\n <button type=\"submit\">Konto erstellen</button>\n</form>",
|
||||||
|
"solution": "<form>\n <div class=\"form-group\">\n <label for=\"password\">Passwort:</label>\n <input type=\"password\" id=\"password\" name=\"password\" required minlength=\"8\" maxlength=\"20\" placeholder=\"Passwort eingeben\">\n <div class=\"hint\">Muss 8-20 Zeichen lang sein</div>\n </div>\n <button type=\"submit\">Konto erstellen</button>\n</form>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "input[type='password']", "attr": "minlength", "value": "8" },
|
||||||
|
"message": "Füge minlength=\"8\" zur Passwort-Eingabe hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "input[type='password']", "attr": "maxlength", "value": "20" },
|
||||||
|
"message": "Füge maxlength=\"20\" zur Passwort-Eingabe hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "input[type='password']", "attr": "placeholder", "value": null },
|
||||||
|
"message": "Füge einen Placeholder hinzu, der andeutet, was einzugeben ist"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "complete-registration",
|
||||||
|
"title": "Vollständiges Registrierungsformular",
|
||||||
|
"description": "Erstelle ein vollständiges Registrierungsformular mit allen Validierungskonzepten:<br><br>- Pflichtfelder mit * markiert<br>- E-Mail-Validierung (type=\"email\" verwenden)<br>- Passwort mit Längenbeschränkungen<br>- AGB-Checkbox (Pflichtfeld)<br>- Absende-Button",
|
||||||
|
"task": "Vervollständige das Registrierungsformular. Füge required-Attribute, passende Eingabetypen und Validierungsbeschränkungen hinzu.",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 400px; background: #f5f5f5; padding: 25px; border-radius: 8px; } h2 { margin-top: 0; margin-bottom: 20px; } .form-group { margin-bottom: 18px; } label { display: block; margin-bottom: 5px; font-weight: 500; } .required { color: #d32f2f; } input[type='text'], input[type='email'], input[type='password'] { width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; font-size: 14px; } input:focus { outline: 2px solid #1976d2; border-color: transparent; } .checkbox-group { display: flex; align-items: flex-start; gap: 8px; } .checkbox-group input { width: auto; margin-top: 3px; } .checkbox-group label { margin: 0; font-weight: normal; } button { width: 100%; padding: 12px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; font-weight: 500; } button:hover { background: #1565c0; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "<form>\n <h2>Konto erstellen</h2>\n \n <div class=\"form-group\">\n <label for=\"fullname\">Vollständiger Name <span class=\"required\">*</span></label>\n <input type=\"text\" id=\"fullname\" name=\"fullname\">\n </div>\n \n <div class=\"form-group\">\n <label for=\"email\">E-Mail <span class=\"required\">*</span></label>\n <input id=\"email\" name=\"email\">\n </div>\n \n <div class=\"form-group\">\n <label for=\"password\">Passwort <span class=\"required\">*</span></label>\n <input id=\"password\" name=\"password\">\n </div>\n \n <div class=\"form-group checkbox-group\">\n <input type=\"checkbox\" id=\"terms\" name=\"terms\">\n <label for=\"terms\">Ich stimme den Nutzungsbedingungen zu <span class=\"required\">*</span></label>\n </div>\n \n <button type=\"submit\">Registrieren</button>\n</form>",
|
||||||
|
"solution": "<form>\n <h2>Konto erstellen</h2>\n \n <div class=\"form-group\">\n <label for=\"fullname\">Vollständiger Name <span class=\"required\">*</span></label>\n <input type=\"text\" id=\"fullname\" name=\"fullname\" required>\n </div>\n \n <div class=\"form-group\">\n <label for=\"email\">E-Mail <span class=\"required\">*</span></label>\n <input type=\"email\" id=\"email\" name=\"email\" required>\n </div>\n \n <div class=\"form-group\">\n <label for=\"password\">Passwort <span class=\"required\">*</span></label>\n <input type=\"password\" id=\"password\" name=\"password\" required minlength=\"8\">\n </div>\n \n <div class=\"form-group checkbox-group\">\n <input type=\"checkbox\" id=\"terms\" name=\"terms\" required>\n <label for=\"terms\">Ich stimme den Nutzungsbedingungen zu <span class=\"required\">*</span></label>\n </div>\n \n <button type=\"submit\">Registrieren</button>\n</form>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "#fullname", "attr": "required", "value": true },
|
||||||
|
"message": "Mache das Feld für den vollständigen Namen zum Pflichtfeld"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "#email", "attr": "type", "value": "email" },
|
||||||
|
"message": "Setze den Eingabetyp für E-Mail auf 'email'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "#email", "attr": "required", "value": true },
|
||||||
|
"message": "Mache das E-Mail-Feld zum Pflichtfeld"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "#password", "attr": "type", "value": "password" },
|
||||||
|
"message": "Setze den Eingabetyp für Passwort auf 'password'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "#password", "attr": "required", "value": true },
|
||||||
|
"message": "Mache das Passwort-Feld zum Pflichtfeld"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "#password", "attr": "minlength", "value": "8" },
|
||||||
|
"message": "Füge minlength=\"8\" zum Passwort hinzu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "#terms", "attr": "required", "value": true },
|
||||||
|
"message": "Mache die AGB-Checkbox zum Pflichtfeld"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
611
src/app.de.js
Normal file
611
src/app.de.js
Normal file
@@ -0,0 +1,611 @@
|
|||||||
|
import { LessonEngine } from "./impl/LessonEngine.js";
|
||||||
|
import { CodeEditor } from "./impl/CodeEditor.js";
|
||||||
|
import { renderLesson, renderModuleList, renderLevelIndicator, updateActiveLessonInSidebar } from "./helpers/renderer.js";
|
||||||
|
import { loadModules } from "./config/lessons.de.js";
|
||||||
|
|
||||||
|
// Simplified state - LessonEngine now manages lesson state and progress
|
||||||
|
const state = {
|
||||||
|
userSettings: {
|
||||||
|
disableFeedbackErrors: false
|
||||||
|
},
|
||||||
|
showExpected: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// DOM elements - updated for new layout
|
||||||
|
const elements = {
|
||||||
|
// Header
|
||||||
|
menuBtn: document.getElementById("menu-btn"),
|
||||||
|
helpBtn: document.getElementById("help-btn"),
|
||||||
|
|
||||||
|
// Left panel
|
||||||
|
lessonTitle: document.getElementById("lesson-title"),
|
||||||
|
lessonDescription: document.getElementById("lesson-description"),
|
||||||
|
taskInstruction: document.getElementById("task-instruction"),
|
||||||
|
codeInput: document.getElementById("code-input"),
|
||||||
|
runBtn: document.getElementById("run-btn"),
|
||||||
|
undoBtn: document.getElementById("undo-btn"),
|
||||||
|
redoBtn: document.getElementById("redo-btn"),
|
||||||
|
resetCodeBtn: document.getElementById("reset-code-btn"),
|
||||||
|
hintArea: document.getElementById("hint-area"),
|
||||||
|
editorContent: document.querySelector(".editor-content"),
|
||||||
|
codeEditor: document.querySelector(".code-editor"),
|
||||||
|
|
||||||
|
// Right panel
|
||||||
|
previewArea: document.getElementById("preview-area"),
|
||||||
|
showExpectedBtn: document.getElementById("show-expected-btn"),
|
||||||
|
expectedOverlay: document.getElementById("expected-overlay"),
|
||||||
|
previewWrapper: document.querySelector(".preview-wrapper"),
|
||||||
|
prevBtn: document.getElementById("prev-btn"),
|
||||||
|
nextBtn: document.getElementById("next-btn"),
|
||||||
|
levelIndicator: document.getElementById("level-indicator"),
|
||||||
|
|
||||||
|
// Sidebar
|
||||||
|
sidebarDrawer: document.getElementById("sidebar-drawer"),
|
||||||
|
sidebarBackdrop: document.getElementById("sidebar-backdrop"),
|
||||||
|
closeSidebar: document.getElementById("close-sidebar"),
|
||||||
|
moduleList: document.getElementById("module-list"),
|
||||||
|
progressFill: document.getElementById("progress-fill"),
|
||||||
|
progressText: document.getElementById("progress-text"),
|
||||||
|
resetBtn: document.getElementById("reset-btn"),
|
||||||
|
disableFeedbackToggle: document.getElementById("disable-feedback-toggle"),
|
||||||
|
|
||||||
|
// Modal
|
||||||
|
modalContainer: document.getElementById("modal-container"),
|
||||||
|
modalTitle: document.getElementById("modal-title"),
|
||||||
|
modalContent: document.getElementById("modal-content"),
|
||||||
|
modalClose: document.getElementById("modal-close")
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize the lesson engine - now the single source of truth
|
||||||
|
const lessonEngine = new LessonEngine();
|
||||||
|
|
||||||
|
// Code editor instance (initialized later)
|
||||||
|
let codeEditor = null;
|
||||||
|
let currentMode = "css";
|
||||||
|
|
||||||
|
// ================= SIDEBAR FUNCTIONS =================
|
||||||
|
|
||||||
|
function openSidebar() {
|
||||||
|
elements.sidebarDrawer.classList.add("open");
|
||||||
|
elements.sidebarBackdrop.classList.add("visible");
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSidebar() {
|
||||||
|
elements.sidebarDrawer.classList.remove("open");
|
||||||
|
elements.sidebarBackdrop.classList.remove("visible");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= EXPECTED RESULT TOGGLE =================
|
||||||
|
|
||||||
|
function toggleExpectedResult() {
|
||||||
|
state.showExpected = !state.showExpected;
|
||||||
|
|
||||||
|
if (state.showExpected) {
|
||||||
|
elements.expectedOverlay.classList.add("visible");
|
||||||
|
elements.showExpectedBtn.textContent = "Lösung ausblenden";
|
||||||
|
elements.showExpectedBtn.classList.add("btn-primary");
|
||||||
|
} else {
|
||||||
|
elements.expectedOverlay.classList.remove("visible");
|
||||||
|
elements.showExpectedBtn.textContent = "Lösung zeigen";
|
||||||
|
elements.showExpectedBtn.classList.remove("btn-primary");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= HINT SYSTEM =================
|
||||||
|
|
||||||
|
function showHint(message, step, total, isSuccess = false) {
|
||||||
|
const hintClass = isSuccess ? "hint hint-success" : "hint";
|
||||||
|
elements.hintArea.innerHTML = `
|
||||||
|
<div class="${hintClass}">
|
||||||
|
<span class="hint-progress">${step}/${total}</span>
|
||||||
|
<span class="hint-message">${message}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearHint() {
|
||||||
|
elements.hintArea.innerHTML = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSuccessHint(message) {
|
||||||
|
elements.hintArea.innerHTML = `
|
||||||
|
<div class="hint hint-success">
|
||||||
|
<span class="hint-progress">✓</span>
|
||||||
|
<span class="hint-message">${message}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= PROGRESS DISPLAY =================
|
||||||
|
|
||||||
|
function updateProgressDisplay() {
|
||||||
|
const stats = lessonEngine.getProgressStats();
|
||||||
|
elements.progressFill.style.width = `${stats.percentComplete}%`;
|
||||||
|
elements.progressText.textContent = `${stats.percentComplete}% abgeschlossen (${stats.totalCompleted}/${stats.totalLessons})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= USER SETTINGS =================
|
||||||
|
|
||||||
|
function loadUserSettings() {
|
||||||
|
const savedSettings = localStorage.getItem("codeCrispies.settings");
|
||||||
|
if (savedSettings) {
|
||||||
|
try {
|
||||||
|
const settings = JSON.parse(savedSettings);
|
||||||
|
state.userSettings = { ...state.userSettings, ...settings };
|
||||||
|
elements.disableFeedbackToggle.checked = !state.userSettings.disableFeedbackErrors;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Fehler beim Laden der Einstellungen:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveUserSettings() {
|
||||||
|
localStorage.setItem("codeCrispies.settings", JSON.stringify(state.userSettings));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= MODULE INITIALIZATION =================
|
||||||
|
|
||||||
|
async function initializeModules() {
|
||||||
|
try {
|
||||||
|
const modules = await loadModules();
|
||||||
|
lessonEngine.setModules(modules);
|
||||||
|
|
||||||
|
// Use the new renderModuleList function with both callbacks
|
||||||
|
renderModuleList(elements.moduleList, modules, selectModule, selectLesson);
|
||||||
|
|
||||||
|
// Load saved progress and select appropriate module
|
||||||
|
const progressData = lessonEngine.loadUserProgress();
|
||||||
|
const lastModuleId = progressData?.lastModuleId;
|
||||||
|
|
||||||
|
if (lastModuleId && modules.find((m) => m.id === lastModuleId)) {
|
||||||
|
selectModule(lastModuleId);
|
||||||
|
} else if (modules.length > 0) {
|
||||||
|
selectModule(modules[0].id);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProgressDisplay();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Module konnten nicht geladen werden:", error);
|
||||||
|
elements.lessonDescription.textContent = "Module konnten nicht geladen werden. Bitte Seite neu laden.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= MODULE/LESSON SELECTION =================
|
||||||
|
|
||||||
|
function selectModule(moduleId) {
|
||||||
|
const success = lessonEngine.setModuleById(moduleId);
|
||||||
|
if (!success) return;
|
||||||
|
|
||||||
|
// Update module list UI to highlight the active module
|
||||||
|
const moduleItems = elements.moduleList.querySelectorAll(".module-header");
|
||||||
|
moduleItems.forEach((item) => {
|
||||||
|
item.classList.remove("active");
|
||||||
|
if (item.dataset.moduleId === moduleId) {
|
||||||
|
item.classList.add("active");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
loadCurrentLesson();
|
||||||
|
resetSuccessIndicators();
|
||||||
|
|
||||||
|
// Close sidebar after selection on mobile
|
||||||
|
if (window.innerWidth <= 768) {
|
||||||
|
closeSidebar();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectLesson(moduleId, lessonIndex) {
|
||||||
|
const currentState = lessonEngine.getCurrentState();
|
||||||
|
if (!currentState.module || currentState.module.id !== moduleId) {
|
||||||
|
lessonEngine.setModuleById(moduleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
lessonEngine.setLessonByIndex(lessonIndex);
|
||||||
|
loadCurrentLesson();
|
||||||
|
|
||||||
|
// Close sidebar after selection on mobile
|
||||||
|
if (window.innerWidth <= 768) {
|
||||||
|
closeSidebar();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= LESSON LOADING =================
|
||||||
|
|
||||||
|
function resetSuccessIndicators() {
|
||||||
|
elements.codeEditor.classList.remove("success-highlight");
|
||||||
|
elements.lessonTitle.classList.remove("success-text");
|
||||||
|
elements.nextBtn.classList.remove("success");
|
||||||
|
elements.taskInstruction.classList.remove("success-instruction");
|
||||||
|
elements.runBtn.classList.remove("success");
|
||||||
|
elements.previewWrapper?.classList.remove("matched");
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateEditorForMode(mode) {
|
||||||
|
const editorLabel = document.querySelector(".editor-label");
|
||||||
|
|
||||||
|
const modeConfig = {
|
||||||
|
html: {
|
||||||
|
placeholder: "HTML hier eingeben... Probiere: nav>ul>li*3 dann Tab drücken",
|
||||||
|
label: "HTML-Editor",
|
||||||
|
cmMode: "html"
|
||||||
|
},
|
||||||
|
tailwind: {
|
||||||
|
placeholder: "Tailwind-Klassen eingeben (z.B. bg-blue-500 text-white p-4)",
|
||||||
|
label: "Tailwind-Klassen",
|
||||||
|
cmMode: "css"
|
||||||
|
},
|
||||||
|
css: {
|
||||||
|
placeholder: "CSS-Code hier eingeben...",
|
||||||
|
label: "CSS-Editor",
|
||||||
|
cmMode: "css"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = modeConfig[mode] || modeConfig.css;
|
||||||
|
if (editorLabel) editorLabel.textContent = config.label;
|
||||||
|
|
||||||
|
// Update CodeMirror mode if needed
|
||||||
|
if (codeEditor && currentMode !== config.cmMode) {
|
||||||
|
currentMode = config.cmMode;
|
||||||
|
codeEditor.setMode(config.cmMode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadCurrentLesson() {
|
||||||
|
const engineState = lessonEngine.getCurrentState();
|
||||||
|
|
||||||
|
if (!engineState.module || !engineState.lesson) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lesson = engineState.lesson;
|
||||||
|
const mode = lesson.mode || engineState.module?.mode || "css";
|
||||||
|
|
||||||
|
// Update UI based on mode
|
||||||
|
updateEditorForMode(mode);
|
||||||
|
|
||||||
|
// Reset any success indicators
|
||||||
|
resetSuccessIndicators();
|
||||||
|
|
||||||
|
// Clear hints
|
||||||
|
clearHint();
|
||||||
|
|
||||||
|
// Hide expected overlay
|
||||||
|
state.showExpected = false;
|
||||||
|
elements.expectedOverlay.classList.remove("visible");
|
||||||
|
elements.showExpectedBtn.textContent = "Lösung zeigen";
|
||||||
|
elements.showExpectedBtn.classList.remove("btn-primary");
|
||||||
|
|
||||||
|
// Update UI
|
||||||
|
renderLesson(
|
||||||
|
elements.lessonTitle,
|
||||||
|
elements.lessonDescription,
|
||||||
|
elements.taskInstruction,
|
||||||
|
elements.previewArea,
|
||||||
|
null, // editorPrefix no longer used
|
||||||
|
null, // codeInput no longer used (using CodeMirror)
|
||||||
|
null, // editorSuffix no longer used
|
||||||
|
lesson
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set user code in CodeMirror
|
||||||
|
if (codeEditor) {
|
||||||
|
codeEditor.setValue(engineState.userCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update Run button text based on completion status
|
||||||
|
if (engineState.isCompleted) {
|
||||||
|
elements.runBtn.innerHTML = '<img src="./gear.svg" alt="" />Erneut';
|
||||||
|
|
||||||
|
// Add completion badge if not present
|
||||||
|
if (!document.querySelector(".completion-badge")) {
|
||||||
|
const badge = document.createElement("span");
|
||||||
|
badge.className = "completion-badge";
|
||||||
|
badge.textContent = "Erledigt";
|
||||||
|
elements.lessonTitle.appendChild(badge);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
elements.runBtn.innerHTML = '<img src="./gear.svg" alt="" />Ausführen';
|
||||||
|
|
||||||
|
// Remove completion badge if exists
|
||||||
|
const badge = document.querySelector(".completion-badge");
|
||||||
|
if (badge) badge.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update level indicator
|
||||||
|
renderLevelIndicator(elements.levelIndicator, engineState.lessonIndex + 1, engineState.totalLessons);
|
||||||
|
|
||||||
|
// Update active lesson in sidebar
|
||||||
|
updateActiveLessonInSidebar(engineState.module.id, engineState.lessonIndex);
|
||||||
|
|
||||||
|
// Update navigation buttons
|
||||||
|
updateNavigationButtons();
|
||||||
|
|
||||||
|
// Update progress display
|
||||||
|
updateProgressDisplay();
|
||||||
|
|
||||||
|
// Focus on the code editor
|
||||||
|
if (codeEditor) {
|
||||||
|
codeEditor.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the expected/solution preview
|
||||||
|
lessonEngine.renderExpectedPreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= LIVE PREVIEW =================
|
||||||
|
|
||||||
|
let previewTimer = null;
|
||||||
|
|
||||||
|
function handleEditorChange(code) {
|
||||||
|
if (previewTimer) {
|
||||||
|
clearTimeout(previewTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
previewTimer = setTimeout(() => {
|
||||||
|
runCode();
|
||||||
|
}, 800);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= NAVIGATION =================
|
||||||
|
|
||||||
|
function updateNavigationButtons() {
|
||||||
|
const engineState = lessonEngine.getCurrentState();
|
||||||
|
|
||||||
|
elements.prevBtn.disabled = !engineState.canGoPrev;
|
||||||
|
elements.nextBtn.disabled = !engineState.canGoNext;
|
||||||
|
|
||||||
|
elements.prevBtn.classList.toggle("btn-disabled", !engineState.canGoPrev);
|
||||||
|
elements.nextBtn.classList.toggle("btn-disabled", !engineState.canGoNext);
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextLesson() {
|
||||||
|
const success = lessonEngine.nextLesson();
|
||||||
|
if (success) {
|
||||||
|
loadCurrentLesson();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function prevLesson() {
|
||||||
|
const success = lessonEngine.previousLesson();
|
||||||
|
if (success) {
|
||||||
|
loadCurrentLesson();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= CODE EXECUTION =================
|
||||||
|
|
||||||
|
function resetCode() {
|
||||||
|
// Reset editor to initial code for current lesson
|
||||||
|
lessonEngine.reset();
|
||||||
|
const engineState = lessonEngine.getCurrentState();
|
||||||
|
if (codeEditor && engineState.lesson) {
|
||||||
|
codeEditor.setValue(engineState.lesson.initialCode || "");
|
||||||
|
}
|
||||||
|
// Clear hints and success indicators
|
||||||
|
clearHint();
|
||||||
|
resetSuccessIndicators();
|
||||||
|
}
|
||||||
|
|
||||||
|
function runCode() {
|
||||||
|
const userCode = codeEditor ? codeEditor.getValue() : "";
|
||||||
|
|
||||||
|
// Rotate the Run button icon
|
||||||
|
const runButtonImg = document.querySelector("#run-btn img");
|
||||||
|
if (runButtonImg) {
|
||||||
|
const currentRotation = parseInt(runButtonImg.style.transform?.match(/\d+/)?.[0] || "0");
|
||||||
|
runButtonImg.style.transform = `rotate(${currentRotation + 180}deg)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply the code to the preview via LessonEngine
|
||||||
|
lessonEngine.applyUserCode(userCode, true);
|
||||||
|
|
||||||
|
// Validate code using LessonEngine
|
||||||
|
const validationResult = lessonEngine.validateCode();
|
||||||
|
|
||||||
|
if (validationResult.isValid) {
|
||||||
|
// Show success hint
|
||||||
|
showSuccessHint(validationResult.message || "Super! Dein Code funktioniert korrekt.");
|
||||||
|
|
||||||
|
// Update Run button
|
||||||
|
elements.runBtn.innerHTML = '<img src="./gear.svg" alt="" />Erneut';
|
||||||
|
elements.runBtn.classList.add("success");
|
||||||
|
|
||||||
|
// Add completion badge
|
||||||
|
if (!document.querySelector(".completion-badge")) {
|
||||||
|
const badge = document.createElement("span");
|
||||||
|
badge.className = "completion-badge";
|
||||||
|
badge.textContent = "Erledigt";
|
||||||
|
elements.lessonTitle.appendChild(badge);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add success visual indicators
|
||||||
|
elements.codeEditor.classList.add("success-highlight");
|
||||||
|
elements.lessonTitle.classList.add("success-text");
|
||||||
|
elements.nextBtn.classList.add("success");
|
||||||
|
elements.taskInstruction.classList.add("success-instruction");
|
||||||
|
|
||||||
|
// Show match animation
|
||||||
|
elements.previewWrapper?.classList.add("matched");
|
||||||
|
setTimeout(() => {
|
||||||
|
elements.previewWrapper?.classList.remove("matched");
|
||||||
|
}, 2500);
|
||||||
|
|
||||||
|
updateNavigationButtons();
|
||||||
|
updateProgressDisplay();
|
||||||
|
} else {
|
||||||
|
// Reset success indicators
|
||||||
|
resetSuccessIndicators();
|
||||||
|
|
||||||
|
// Show hint with step progress
|
||||||
|
const step = validationResult.validCases + 1;
|
||||||
|
const total = validationResult.totalCases;
|
||||||
|
|
||||||
|
// Only show hints if enabled
|
||||||
|
if (!state.userSettings.disableFeedbackErrors) {
|
||||||
|
showHint(validationResult.message || "Weiter versuchen!", step, total);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= MODALS =================
|
||||||
|
|
||||||
|
function showHelp() {
|
||||||
|
elements.modalTitle.textContent = "Hilfe";
|
||||||
|
|
||||||
|
elements.modalContent.innerHTML = `
|
||||||
|
<h3>So verwendest du Code Crispies</h3>
|
||||||
|
<p>Code Crispies ist eine interaktive Plattform zum Erlernen von HTML, CSS und Tailwind durch praktische Übungen.</p>
|
||||||
|
|
||||||
|
<h4>Erste Schritte</h4>
|
||||||
|
<p>Öffne das Menü (☰), um ein Lektionsmodul auszuwählen. Jedes Modul enthält eine Reihe von Lektionen.</p>
|
||||||
|
|
||||||
|
<h4>Lektionen abschließen</h4>
|
||||||
|
<ol>
|
||||||
|
<li>Lies die Anleitung auf der linken Seite</li>
|
||||||
|
<li>Schreibe deinen Code im Editor</li>
|
||||||
|
<li>Klicke auf "Ausführen" oder drücke Strg+Enter zum Testen</li>
|
||||||
|
<li>Folge den Hinweisen, um Probleme zu beheben</li>
|
||||||
|
<li>Klicke auf "Weiter", wenn du fertig bist</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h4>Tipps</h4>
|
||||||
|
<ul>
|
||||||
|
<li>Klicke auf "Lösung zeigen", um das Zielergebnis zu sehen</li>
|
||||||
|
<li>Dein Fortschritt wird automatisch gespeichert</li>
|
||||||
|
<li>Strg+Enter führt deinen Code aus</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h4>Emmet-Kürzel (HTML-Modus)</h4>
|
||||||
|
<p>Tippe Abkürzungen ein und drücke Tab zum Erweitern:</p>
|
||||||
|
<ul>
|
||||||
|
<li><kbd>div.container</kbd> → div mit Klasse</li>
|
||||||
|
<li><kbd>ul>li*5</kbd> → ul mit 5 li-Kindern</li>
|
||||||
|
<li><kbd>nav>ul>li*3>a</kbd> → verschachtelte Struktur</li>
|
||||||
|
<li><kbd>p{Hallo}</kbd> → p mit Textinhalt</li>
|
||||||
|
</ul>
|
||||||
|
`;
|
||||||
|
|
||||||
|
elements.modalContainer.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
function showResetConfirmation() {
|
||||||
|
elements.modalTitle.textContent = "Fortschritt zurücksetzen";
|
||||||
|
|
||||||
|
elements.modalContent.innerHTML = `
|
||||||
|
<p>Bist du sicher, dass du deinen gesamten Fortschritt zurücksetzen möchtest? Dies kann nicht rückgängig gemacht werden.</p>
|
||||||
|
<div style="display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px;">
|
||||||
|
<button id="cancel-reset" class="btn">Abbrechen</button>
|
||||||
|
<button id="confirm-reset" class="btn btn-ghost">Alles zurücksetzen</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.getElementById("cancel-reset").addEventListener("click", closeModal);
|
||||||
|
document.getElementById("confirm-reset").addEventListener("click", () => {
|
||||||
|
lessonEngine.clearProgress();
|
||||||
|
closeModal();
|
||||||
|
closeSidebar();
|
||||||
|
|
||||||
|
// Reload first module
|
||||||
|
const modules = lessonEngine.modules;
|
||||||
|
if (modules.length > 0) {
|
||||||
|
selectModule(modules[0].id);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProgressDisplay();
|
||||||
|
});
|
||||||
|
|
||||||
|
elements.modalContainer.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
elements.modalContainer.classList.add("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= INITIALIZATION =================
|
||||||
|
|
||||||
|
function initCodeEditor() {
|
||||||
|
const container = elements.editorContent;
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
// Remove the textarea - CodeMirror will replace it
|
||||||
|
const textarea = container.querySelector("textarea");
|
||||||
|
if (textarea) {
|
||||||
|
textarea.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize CodeMirror
|
||||||
|
codeEditor = new CodeEditor(container, {
|
||||||
|
mode: currentMode,
|
||||||
|
placeholder: "Code hier eingeben...",
|
||||||
|
onChange: handleEditorChange
|
||||||
|
});
|
||||||
|
|
||||||
|
codeEditor.init("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
loadUserSettings();
|
||||||
|
|
||||||
|
// Initialize CodeMirror editor
|
||||||
|
initCodeEditor();
|
||||||
|
|
||||||
|
// Load modules after editor is ready
|
||||||
|
initializeModules().catch(console.error);
|
||||||
|
|
||||||
|
// Sidebar controls
|
||||||
|
elements.menuBtn.addEventListener("click", openSidebar);
|
||||||
|
elements.closeSidebar.addEventListener("click", closeSidebar);
|
||||||
|
elements.sidebarBackdrop.addEventListener("click", closeSidebar);
|
||||||
|
|
||||||
|
// Expected result toggle
|
||||||
|
elements.showExpectedBtn.addEventListener("click", toggleExpectedResult);
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
elements.prevBtn.addEventListener("click", prevLesson);
|
||||||
|
elements.nextBtn.addEventListener("click", nextLesson);
|
||||||
|
elements.runBtn.addEventListener("click", runCode);
|
||||||
|
|
||||||
|
// Editor tools
|
||||||
|
elements.undoBtn.addEventListener("click", () => {
|
||||||
|
if (codeEditor) codeEditor.undo();
|
||||||
|
});
|
||||||
|
elements.redoBtn.addEventListener("click", () => {
|
||||||
|
if (codeEditor) codeEditor.redo();
|
||||||
|
});
|
||||||
|
elements.resetCodeBtn.addEventListener("click", resetCode);
|
||||||
|
|
||||||
|
// Modals
|
||||||
|
elements.helpBtn.addEventListener("click", showHelp);
|
||||||
|
elements.modalClose.addEventListener("click", closeModal);
|
||||||
|
elements.resetBtn.addEventListener("click", showResetConfirmation);
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
elements.disableFeedbackToggle.addEventListener("change", (e) => {
|
||||||
|
state.userSettings.disableFeedbackErrors = !e.target.checked;
|
||||||
|
saveUserSettings();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click on editor content to focus CodeMirror
|
||||||
|
elements.editorContent?.addEventListener("click", () => {
|
||||||
|
if (codeEditor) codeEditor.focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keyboard shortcuts
|
||||||
|
document.addEventListener("keydown", (e) => {
|
||||||
|
// Ctrl+Enter to run code
|
||||||
|
if (e.ctrlKey && e.key === "Enter") {
|
||||||
|
runCode();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escape to close sidebar
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
closeSidebar();
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the application
|
||||||
|
init();
|
||||||
111
src/config/lessons.de.js
Normal file
111
src/config/lessons.de.js
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
/**
|
||||||
|
* Lesson Config (German) - Functions for loading lesson configurations
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Import German lesson configs
|
||||||
|
import basicSelectorsConfig from "../../lessons/de/00-basic-selectors.json";
|
||||||
|
import advancedSelectorsConfig from "../../lessons/de/01-advanced-selectors.json";
|
||||||
|
import tailwindConfig from "../../lessons/de/10-tailwind-basics.json";
|
||||||
|
// HTML lessons
|
||||||
|
import htmlElementsConfig from "../../lessons/de/20-html-elements.json";
|
||||||
|
import htmlFormsBasicConfig from "../../lessons/de/21-html-forms-basic.json";
|
||||||
|
import htmlFormsValidationConfig from "../../lessons/de/22-html-forms-validation.json";
|
||||||
|
|
||||||
|
// Module store
|
||||||
|
const moduleStore = [
|
||||||
|
htmlElementsConfig,
|
||||||
|
htmlFormsBasicConfig,
|
||||||
|
htmlFormsValidationConfig,
|
||||||
|
basicSelectorsConfig,
|
||||||
|
advancedSelectorsConfig,
|
||||||
|
tailwindConfig
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load all available modules
|
||||||
|
* @returns {Promise<Array>} Promise resolving to array of modules
|
||||||
|
*/
|
||||||
|
export async function loadModules() {
|
||||||
|
return moduleStore.map((module) => ({
|
||||||
|
...module,
|
||||||
|
lessons: module.lessons.map((lesson) => ({
|
||||||
|
...lesson,
|
||||||
|
mode: module.mode || "css"
|
||||||
|
}))
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a module by its ID
|
||||||
|
* @param {string} moduleId - The module ID to find
|
||||||
|
* @returns {Object|null} The module object or null if not found
|
||||||
|
*/
|
||||||
|
export function getModuleById(moduleId) {
|
||||||
|
return moduleStore.find((module) => module.id === moduleId) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load module configs from a URL
|
||||||
|
* @param {string} url - URL to load the config from
|
||||||
|
* @returns {Promise<Object>} Promise resolving to the module config
|
||||||
|
*/
|
||||||
|
export async function loadModuleFromUrl(url) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to load module: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const moduleConfig = await response.json();
|
||||||
|
validateModuleConfig(moduleConfig);
|
||||||
|
|
||||||
|
return moduleConfig;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading module from URL:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate a module configuration
|
||||||
|
* @param {Object} config - The module configuration to validate
|
||||||
|
* @throws {Error} If the configuration is invalid
|
||||||
|
*/
|
||||||
|
function validateModuleConfig(config) {
|
||||||
|
// Required fields
|
||||||
|
if (!config.id) throw new Error('Module config missing "id"');
|
||||||
|
if (!config.title) throw new Error('Module config missing "title"');
|
||||||
|
if (!Array.isArray(config.lessons)) throw new Error('Module config missing "lessons" array');
|
||||||
|
|
||||||
|
// Check each lesson
|
||||||
|
config.lessons.forEach((lesson, index) => {
|
||||||
|
if (!lesson.title) throw new Error(`Lesson ${index} missing "title"`);
|
||||||
|
if (!lesson.previewHTML) throw new Error(`Lesson ${index} missing "previewHTML"`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a custom module to the store
|
||||||
|
* @param {Object} moduleConfig - The module configuration to add
|
||||||
|
* @returns {boolean} Success status
|
||||||
|
*/
|
||||||
|
export function addCustomModule(moduleConfig) {
|
||||||
|
try {
|
||||||
|
validateModuleConfig(moduleConfig);
|
||||||
|
|
||||||
|
// Check if module with same ID already exists
|
||||||
|
const existingIndex = moduleStore.findIndex((m) => m.id === moduleConfig.id);
|
||||||
|
if (existingIndex >= 0) {
|
||||||
|
// Replace existing module
|
||||||
|
moduleStore[existingIndex] = moduleConfig;
|
||||||
|
} else {
|
||||||
|
// Add new module
|
||||||
|
moduleStore.push(moduleConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error adding custom module:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
152
src/index.de.html
Normal file
152
src/index.de.html
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" href="./favicon.ico" type="image/x-icon" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>CODE CRISPIES - CSS interaktiv lernen</title>
|
||||||
|
<link rel="stylesheet" href="main.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app-container">
|
||||||
|
<!-- Minimaler Header -->
|
||||||
|
<header class="header">
|
||||||
|
<button id="menu-btn" class="menu-toggle" aria-label="Menü öffnen">
|
||||||
|
<span class="hamburger-line"></span>
|
||||||
|
<span class="hamburger-line"></span>
|
||||||
|
<span class="hamburger-line"></span>
|
||||||
|
</button>
|
||||||
|
<div class="logo">
|
||||||
|
<img src="./bowl.png" width="40" alt="CODE CRISPIES Logo" />
|
||||||
|
<h1>CODE<span>CRISPIES</span></h1>
|
||||||
|
</div>
|
||||||
|
<button id="help-btn" class="help-toggle" aria-label="Hilfe">?</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Hauptlayout -->
|
||||||
|
<main class="game-layout">
|
||||||
|
<!-- Linke Spalte: Anleitung + Editor -->
|
||||||
|
<div class="left-panel">
|
||||||
|
<section class="instructions">
|
||||||
|
<h2 id="lesson-title">Laden...</h2>
|
||||||
|
<div class="lesson-description" id="lesson-description">
|
||||||
|
Bitte wähle eine Lektion aus, um zu beginnen.
|
||||||
|
</div>
|
||||||
|
<div class="task-instruction" id="task-instruction">
|
||||||
|
<!-- Aufgabenanweisungen werden hier angezeigt -->
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="editor-section">
|
||||||
|
<div class="code-editor">
|
||||||
|
<div class="editor-header">
|
||||||
|
<label for="code-input" class="editor-label">CSS-Editor</label>
|
||||||
|
<div class="editor-actions">
|
||||||
|
<div class="editor-tools">
|
||||||
|
<button id="undo-btn" class="btn btn-icon" title="Rückgängig (Strg+Z)">↶</button>
|
||||||
|
<button id="redo-btn" class="btn btn-icon" title="Wiederholen (Strg+Umschalt+Z)">↷</button>
|
||||||
|
<button id="reset-code-btn" class="btn btn-icon" title="Auf Anfangscode zurücksetzen">⟲</button>
|
||||||
|
</div>
|
||||||
|
<button id="run-btn" class="btn btn-run">
|
||||||
|
<img src="./gear.svg" alt="" />Ausführen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="editor-content">
|
||||||
|
<!-- Textarea wird durch CodeMirror ersetzt -->
|
||||||
|
<textarea id="code-input" class="code-input" spellcheck="false" autocomplete="off" style="display: none;"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hint-area" id="hint-area">
|
||||||
|
<!-- Hinweise werden hier angezeigt -->
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Rechte Spalte: Vorschau + Navigation -->
|
||||||
|
<div class="right-panel">
|
||||||
|
<div class="preview-section">
|
||||||
|
<div class="preview-header">
|
||||||
|
<span class="preview-label">Deine Ausgabe</span>
|
||||||
|
<button id="show-expected-btn" class="btn btn-small">Lösung zeigen</button>
|
||||||
|
</div>
|
||||||
|
<div class="preview-wrapper">
|
||||||
|
<div class="preview-frame" id="preview-area">
|
||||||
|
<!-- Vorschau-Iframe wird hier angezeigt -->
|
||||||
|
</div>
|
||||||
|
<div class="expected-overlay" id="expected-overlay">
|
||||||
|
<div class="expected-frame" id="preview-expected">
|
||||||
|
<!-- Erwartetes Ergebnis (umschaltbar) -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="game-controls">
|
||||||
|
<button id="prev-btn" class="btn">Zurück</button>
|
||||||
|
<div class="level-indicator" id="level-indicator">Lektion 0/0</div>
|
||||||
|
<button id="next-btn" class="btn btn-primary">Weiter</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Seitenleisten-Hintergrund -->
|
||||||
|
<div class="sidebar-backdrop" id="sidebar-backdrop"></div>
|
||||||
|
|
||||||
|
<!-- Ausklappbare Seitenleiste -->
|
||||||
|
<aside class="sidebar-drawer" id="sidebar-drawer">
|
||||||
|
<div class="sidebar-header">
|
||||||
|
<h3>Menü</h3>
|
||||||
|
<button id="close-sidebar" class="close-btn" aria-label="Menü schließen">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-section">
|
||||||
|
<h4>Fortschritt</h4>
|
||||||
|
<div class="progress-display" id="progress-display">
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="progress-fill" id="progress-fill"></div>
|
||||||
|
</div>
|
||||||
|
<span class="progress-text" id="progress-text">0% abgeschlossen</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-section">
|
||||||
|
<h4>Lektionen</h4>
|
||||||
|
<div class="module-list" id="module-list">
|
||||||
|
<!-- Modulliste wird hier eingefügt -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-section">
|
||||||
|
<h4>Einstellungen</h4>
|
||||||
|
<label class="toggle-switch">
|
||||||
|
<input type="checkbox" id="disable-feedback-toggle" checked />
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
<span class="toggle-label">Hinweise anzeigen</span>
|
||||||
|
</label>
|
||||||
|
<button id="reset-btn" class="btn btn-text">Fortschritt zurücksetzen</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="app-footer">
|
||||||
|
Open Source:
|
||||||
|
<a href="https://github.com/nextlevelshit/code-crispies" target="_blank">GitHub</a>
|
||||||
|
von <a href="https://dailysh.it" title="Michael W. Czechowski">mwc</a>
|
||||||
|
</footer>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Hilfe-Modal -->
|
||||||
|
<div id="modal-container" class="modal-container hidden">
|
||||||
|
<div class="modal">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 id="modal-title">Modal-Titel</h3>
|
||||||
|
<button id="modal-close" class="modal-close">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-content" id="modal-content">
|
||||||
|
<!-- Modal-Inhalt wird hier eingefügt -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="module" src="app.de.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user