b88962da6f
Dev Lab bündelt mehrere Disziplinen (SQL, Java/Debugging, Git, HTML, CSS, Regex, PHP, REST) in 11 Spielen mit zusammen 189 Aufgaben. Im Gegensatz zur ersten, rein clientseitigen Skizze jetzt server-autoritativ, DB-gestützt und im Admin pflegbar – mit echter Mechanik-Vielfalt statt überall Lückentext: cloze (Lücke tippen), choice (Multiple Choice), order (Bausteine zusammensetzen), spot (Bug anklicken), match (Paare zuordnen), regex (Muster schreiben mit Live-Treffer-Vorschau). Prüfung und Score ausschließlich serverseitig – die Lösungen verlassen nie das Backend; Eingabe-Platzhalter sind neutrale Labels (Lösungs-Leak behoben). Fehler-Mechanik mit Versuchen + Hinweis + Auflösung, kein Game-Over; Score über die generische game_scores-Tabelle (Kennung devlab, deck=Spiel-Slug). - Entities DevLabGame/DevLabTask (JSON-payload) + Flyway V8__devlab.sql - DevLabService mit sechs Mechanik-Checkern; API /api/devlab/** (CSRF-ausgenommen, devlab in KNOWN_GAMES) - Admin /admin/devlab (Spiele + Aufgaben pflegen); polymorphe Frontend-Engine devlabgame.js + DB-getriebener Hub devlab.js - Inhalte aus devlab/content.json (idempotenter Seeder); ersetzt die erste clientseitige Dev-Lab-Skizze (devlab.js/devlabgame.js/-game.html/-game.css neu) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
3959 lines
123 KiB
JSON
3959 lines
123 KiB
JSON
{
|
|
"games": [
|
|
{
|
|
"slug": "query-builder",
|
|
"title": "Query Builder",
|
|
"language": "SQL",
|
|
"path": "syntax",
|
|
"description": "Baue SQL-Abfragen Stück für Stück zusammen und lerne die Reihenfolge der Klauseln.",
|
|
"tasks": [
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Spalten auswählen",
|
|
"points": 100,
|
|
"narrative": "Jede Abfrage beginnt mit dem richtigen Keyword.",
|
|
"codeTemplate": "{{1}} name, stadt\nFROM kunden;",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Keyword",
|
|
"hint": "Womit wählst du Spalten aus?",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"SELECT"
|
|
]
|
|
}
|
|
],
|
|
"hint": "Das allererste Wort jeder Lese-Abfrage.",
|
|
"successText": "Perfekt, die Spalten sind ausgewählt!"
|
|
},
|
|
{
|
|
"type": "match",
|
|
"prompt": "Klausel und Zweck",
|
|
"points": 100,
|
|
"instruction": "Ordne jede Klausel ihrer Aufgabe zu.",
|
|
"pairs": [
|
|
[
|
|
"WHERE",
|
|
"filtert einzelne Zeilen"
|
|
],
|
|
[
|
|
"GROUP BY",
|
|
"fasst Zeilen zu Gruppen zusammen"
|
|
],
|
|
[
|
|
"HAVING",
|
|
"filtert ganze Gruppen"
|
|
],
|
|
[
|
|
"ORDER BY",
|
|
"sortiert das Ergebnis"
|
|
]
|
|
],
|
|
"hint": "WHERE wirkt VOR der Gruppierung, HAVING danach.",
|
|
"successText": "Alle Klauseln richtig zugeordnet!"
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Der Durchschnitt",
|
|
"points": 100,
|
|
"question": "Welche Aggregatfunktion liefert den Durchschnittswert?",
|
|
"options": [
|
|
"SUM()",
|
|
"AVG()",
|
|
"COUNT()",
|
|
"MAX()"
|
|
],
|
|
"correctIndex": 1,
|
|
"hint": "Average heißt Durchschnitt.",
|
|
"successText": "Richtig, AVG() berechnet den Mittelwert."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Zeilen filtern",
|
|
"points": 100,
|
|
"narrative": "Du suchst nur Kunden aus Leipzig.",
|
|
"codeTemplate": "SELECT name\nFROM kunden\nWHERE stadt {{1}} 'Leipzig';",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Operator",
|
|
"hint": "Test auf Gleichheit.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"="
|
|
]
|
|
}
|
|
],
|
|
"hint": "In SQL prüft man Gleichheit mit einem einzelnen Zeichen.",
|
|
"successText": "Gut gefiltert!"
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Nur Eindeutiges",
|
|
"points": 100,
|
|
"narrative": "Du willst jede Stadt nur einmal sehen.",
|
|
"codeTemplate": "SELECT {{1}} stadt\nFROM kunden;",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Keyword",
|
|
"hint": "Entfernt doppelte Werte.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"DISTINCT"
|
|
]
|
|
}
|
|
],
|
|
"hint": "Das Wort steht direkt hinter SELECT.",
|
|
"successText": "Duplikate verschwunden!"
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Alle Zeilen zählen",
|
|
"points": 100,
|
|
"question": "Wie zählst du die Anzahl aller Zeilen einer Tabelle?",
|
|
"options": [
|
|
"COUNT(*)",
|
|
"SUM(*)",
|
|
"TOTAL()",
|
|
"ROWS()"
|
|
],
|
|
"correctIndex": 0,
|
|
"hint": "Der Stern steht für alle Spalten.",
|
|
"successText": "Genau, COUNT(*) zählt jede Zeile."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Gruppieren",
|
|
"points": 100,
|
|
"narrative": "Wie viele Mitarbeiter hat jede Abteilung?",
|
|
"codeTemplate": "SELECT abteilung, COUNT(*)\nFROM mitarbeiter\n{{1}} abteilung;",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Klausel",
|
|
"hint": "Fasst Zeilen je Abteilung zusammen.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"GROUP BY"
|
|
]
|
|
}
|
|
],
|
|
"hint": "Zwei Wörter mit Leerzeichen.",
|
|
"successText": "Sauber gruppiert!"
|
|
},
|
|
{
|
|
"type": "order",
|
|
"prompt": "Einfache Abfrage bauen",
|
|
"points": 100,
|
|
"instruction": "Bring die Zeilen in die korrekte SQL-Reihenfolge.",
|
|
"items": [
|
|
"SELECT name",
|
|
"FROM kunden",
|
|
"WHERE stadt = 'Berlin'",
|
|
"ORDER BY name"
|
|
],
|
|
"hint": "Erst auswählen, dann woher, dann filtern, dann sortieren.",
|
|
"successText": "Die Abfrage steht richtig!"
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Absteigend sortieren",
|
|
"points": 100,
|
|
"narrative": "Die ältesten Kunden sollen oben stehen.",
|
|
"codeTemplate": "SELECT name, alter\nFROM kunden\nORDER BY alter {{1}};",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Keyword",
|
|
"hint": "Descending = absteigend.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"DESC"
|
|
]
|
|
}
|
|
],
|
|
"hint": "Drei Buchstaben für absteigend.",
|
|
"successText": "Größter Wert zuerst!"
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Vor der Gruppierung filtern",
|
|
"points": 100,
|
|
"question": "Welche Klausel filtert Zeilen, BEVOR gruppiert wird?",
|
|
"options": [
|
|
"HAVING",
|
|
"WHERE",
|
|
"GROUP BY",
|
|
"ORDER BY"
|
|
],
|
|
"correctIndex": 1,
|
|
"hint": "Diese Klausel arbeitet auf einzelnen Zeilen.",
|
|
"successText": "Richtig, WHERE filtert vor der Gruppierung."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Gruppen filtern",
|
|
"points": 100,
|
|
"narrative": "Nur Abteilungen mit mehr als 5 Leuten interessieren dich.",
|
|
"codeTemplate": "SELECT abteilung, COUNT(*)\nFROM mitarbeiter\nGROUP BY abteilung\n{{1}} COUNT(*) > 5;",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Klausel",
|
|
"hint": "Filtert nach dem Gruppieren.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"HAVING"
|
|
]
|
|
}
|
|
],
|
|
"hint": "WHERE geht hier nicht, weil aggregiert wird.",
|
|
"successText": "Nur große Abteilungen bleiben übrig!"
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Nach der Gruppierung",
|
|
"points": 100,
|
|
"question": "Welche Klausel filtert ganze Gruppen NACH GROUP BY?",
|
|
"options": [
|
|
"WHERE",
|
|
"HAVING",
|
|
"LIMIT",
|
|
"ON"
|
|
],
|
|
"correctIndex": 1,
|
|
"hint": "Sie kann Aggregatfunktionen wie COUNT(*) nutzen.",
|
|
"successText": "Korrekt, HAVING filtert Gruppen."
|
|
},
|
|
{
|
|
"type": "match",
|
|
"prompt": "Aggregatfunktionen",
|
|
"points": 100,
|
|
"instruction": "Ordne jede Funktion ihrer Bedeutung zu.",
|
|
"pairs": [
|
|
[
|
|
"COUNT()",
|
|
"Anzahl"
|
|
],
|
|
[
|
|
"SUM()",
|
|
"Summe"
|
|
],
|
|
[
|
|
"AVG()",
|
|
"Durchschnitt"
|
|
],
|
|
[
|
|
"MAX()",
|
|
"Maximum"
|
|
],
|
|
[
|
|
"MIN()",
|
|
"Minimum"
|
|
]
|
|
],
|
|
"hint": "Englische Wörter helfen: count, sum, average ...",
|
|
"successText": "Alle Funktionen sitzen!"
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Ergebnis begrenzen",
|
|
"points": 100,
|
|
"narrative": "Du brauchst nur die ersten 10 Treffer.",
|
|
"codeTemplate": "SELECT name\nFROM kunden\n{{1}} 10;",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Klausel",
|
|
"hint": "Begrenzt die Zeilenzahl.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"LIMIT"
|
|
]
|
|
}
|
|
],
|
|
"hint": "Englisch für Grenze.",
|
|
"successText": "Nur 10 Zeilen, perfekt!"
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Datensatz einfügen",
|
|
"points": 100,
|
|
"narrative": "Ein neuer Kunde kommt in die Tabelle.",
|
|
"codeTemplate": "{{1}} INTO kunden (name, stadt)\n{{2}} ('Anna', 'Berlin');",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Keyword",
|
|
"hint": "Womit fügst du ein?",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"INSERT"
|
|
]
|
|
},
|
|
{
|
|
"position": 2,
|
|
"placeholder": "Keyword",
|
|
"hint": "Leitet die Werteliste ein.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"VALUES"
|
|
]
|
|
}
|
|
],
|
|
"hint": "INSERT INTO ... VALUES (...).",
|
|
"successText": "Anna ist eingetragen!"
|
|
},
|
|
{
|
|
"type": "order",
|
|
"prompt": "Vollständige Abfrage",
|
|
"points": 100,
|
|
"instruction": "Setze die komplette Abfrage in die richtige Reihenfolge.",
|
|
"items": [
|
|
"SELECT abteilung, COUNT(*)",
|
|
"FROM mitarbeiter",
|
|
"WHERE gehalt > 3000",
|
|
"GROUP BY abteilung",
|
|
"HAVING COUNT(*) > 2",
|
|
"ORDER BY abteilung"
|
|
],
|
|
"hint": "SELECT, FROM, WHERE, GROUP BY, HAVING, ORDER BY.",
|
|
"successText": "Die ganze Query steht korrekt!"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"slug": "sql-detective",
|
|
"title": "SQL Detective",
|
|
"language": "SQL",
|
|
"path": "debugging",
|
|
"description": "Löse IT-Ermittlungsfälle, indem du fehlerhafte und fehlende Teile von SQL-Abfragen aufspürst.",
|
|
"tasks": [
|
|
{
|
|
"type": "spot",
|
|
"prompt": "Fall: Falsche Anführungszeichen",
|
|
"points": 100,
|
|
"narrative": "Die Abfrage liefert einen Fehler statt der Berliner Kunden.",
|
|
"instruction": "Klick den fehlerhaften Token an.",
|
|
"tokens": [
|
|
"SELECT",
|
|
"*",
|
|
"FROM",
|
|
"kunden",
|
|
"WHERE",
|
|
"stadt",
|
|
"=",
|
|
"\"Berlin\"",
|
|
";"
|
|
],
|
|
"bugIndex": 7,
|
|
"fix": "'Berlin'",
|
|
"hint": "String-Literale brauchen in SQL einfache Anführungszeichen.",
|
|
"successText": "Gelöst! Strings stehen in einfachen Quotes."
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Fall: NULL-Werte finden",
|
|
"points": 100,
|
|
"question": "Welche Bedingung findet Zeilen ohne Stadt (NULL)?",
|
|
"options": [
|
|
"stadt = NULL",
|
|
"stadt IS NULL",
|
|
"stadt == NULL",
|
|
"stadt NULL()"
|
|
],
|
|
"correctIndex": 1,
|
|
"hint": "Mit = lässt sich NULL nicht vergleichen.",
|
|
"successText": "Richtig, NULL prüft man mit IS NULL."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Fall: Fehlendes JOIN-Schlüsselwort",
|
|
"points": 100,
|
|
"narrative": "Der Verknüpfungs-Befehl fehlt zwischen den Tabellen.",
|
|
"codeTemplate": "SELECT *\nFROM bestellungen b\nJOIN kunden k {{1}} b.kunde_id = k.id;",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Keyword",
|
|
"hint": "Gibt die Verknüpfungsbedingung an.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"ON"
|
|
]
|
|
}
|
|
],
|
|
"hint": "Zwei Buchstaben.",
|
|
"successText": "Die Tabellen sind sauber verbunden!"
|
|
},
|
|
{
|
|
"type": "spot",
|
|
"prompt": "Fall: Kaputter Operator",
|
|
"points": 100,
|
|
"narrative": "Der Vergleichsoperator existiert so nicht.",
|
|
"instruction": "Klick den fehlerhaften Token an.",
|
|
"tokens": [
|
|
"SELECT",
|
|
"name",
|
|
"FROM",
|
|
"kunden",
|
|
"WHERE",
|
|
"alter",
|
|
"=>",
|
|
"18",
|
|
";"
|
|
],
|
|
"bugIndex": 6,
|
|
"fix": ">=",
|
|
"hint": "Größer-oder-gleich schreibt man andersherum.",
|
|
"successText": "Korrekt, der Operator heißt >=."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Fall: NULL-Prüfung ergänzen",
|
|
"points": 100,
|
|
"narrative": "Du suchst Kunden, bei denen keine Stadt hinterlegt ist.",
|
|
"codeTemplate": "SELECT name\nFROM kunden\nWHERE stadt {{1}} NULL;",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Operator",
|
|
"hint": "Nicht = verwenden!",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"IS"
|
|
]
|
|
}
|
|
],
|
|
"hint": "Das Schlüsselwort für NULL-Vergleiche.",
|
|
"successText": "Sauber, IS NULL funktioniert."
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Fall: Der richtige JOIN",
|
|
"points": 100,
|
|
"question": "Welcher JOIN liefert ALLE Zeilen der linken Tabelle, auch ohne Treffer rechts?",
|
|
"options": [
|
|
"INNER JOIN",
|
|
"LEFT JOIN",
|
|
"RIGHT JOIN",
|
|
"CROSS JOIN"
|
|
],
|
|
"correctIndex": 1,
|
|
"hint": "Links steht zuerst.",
|
|
"successText": "Richtig, LEFT JOIN behält alle linken Zeilen."
|
|
},
|
|
{
|
|
"type": "spot",
|
|
"prompt": "Fall: Verlorenes Komma",
|
|
"points": 100,
|
|
"narrative": "Zwischen zwei Spalten hat sich ein falsches Wort eingeschlichen.",
|
|
"instruction": "Klick den fehlerhaften Token an.",
|
|
"tokens": [
|
|
"SELECT",
|
|
"name",
|
|
"AND",
|
|
"stadt",
|
|
"FROM",
|
|
"kunden",
|
|
";"
|
|
],
|
|
"bugIndex": 2,
|
|
"fix": ",",
|
|
"hint": "Spalten werden nicht mit AND getrennt.",
|
|
"successText": "Genau, Spalten trennt ein Komma."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Fall: Muster-Suche",
|
|
"points": 100,
|
|
"narrative": "Du suchst alle Namen, die mit A beginnen.",
|
|
"codeTemplate": "SELECT name\nFROM kunden\nWHERE name {{1}} 'A%';",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Operator",
|
|
"hint": "Für Mustervergleiche mit %.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"LIKE"
|
|
]
|
|
}
|
|
],
|
|
"hint": "Vier Buchstaben für Textmuster.",
|
|
"successText": "Alle A-Namen gefunden!"
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Fall: Nur gemeinsame Treffer",
|
|
"points": 100,
|
|
"question": "Welcher JOIN liefert nur Zeilen, die in BEIDEN Tabellen einen Treffer haben?",
|
|
"options": [
|
|
"LEFT JOIN",
|
|
"FULL JOIN",
|
|
"INNER JOIN",
|
|
"RIGHT JOIN"
|
|
],
|
|
"correctIndex": 2,
|
|
"hint": "Die Schnittmenge beider Tabellen.",
|
|
"successText": "Korrekt, INNER JOIN zeigt nur Übereinstimmungen."
|
|
},
|
|
{
|
|
"type": "spot",
|
|
"prompt": "Fall: NULL falsch verglichen",
|
|
"points": 100,
|
|
"narrative": "Diese Abfrage liefert nie ein Ergebnis, obwohl es NULL-Werte gibt.",
|
|
"instruction": "Klick den fehlerhaften Token an.",
|
|
"tokens": [
|
|
"SELECT",
|
|
"*",
|
|
"FROM",
|
|
"kunden",
|
|
"WHERE",
|
|
"stadt",
|
|
"=",
|
|
"NULL",
|
|
";"
|
|
],
|
|
"bugIndex": 6,
|
|
"fix": "IS",
|
|
"hint": "NULL lässt sich nicht mit = vergleichen.",
|
|
"successText": "Gelöst! IS NULL statt = NULL."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Fall: Bereich abfragen",
|
|
"points": 100,
|
|
"narrative": "Gesucht sind Produkte zwischen 10 und 20 Euro.",
|
|
"codeTemplate": "SELECT *\nFROM produkte\nWHERE preis {{1}} 10 AND 20;",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Keyword",
|
|
"hint": "Für einen Wertebereich inklusive Grenzen.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"BETWEEN"
|
|
]
|
|
}
|
|
],
|
|
"hint": "Englisch für 'zwischen'.",
|
|
"successText": "Bereich korrekt eingegrenzt!"
|
|
},
|
|
{
|
|
"type": "match",
|
|
"prompt": "Fall: JOIN-Typen einordnen",
|
|
"points": 100,
|
|
"instruction": "Ordne jedem JOIN-Typ seine Bedeutung zu.",
|
|
"pairs": [
|
|
[
|
|
"INNER JOIN",
|
|
"nur Treffer in beiden Tabellen"
|
|
],
|
|
[
|
|
"LEFT JOIN",
|
|
"alle links + passende rechts"
|
|
],
|
|
[
|
|
"RIGHT JOIN",
|
|
"alle rechts + passende links"
|
|
],
|
|
[
|
|
"FULL JOIN",
|
|
"alle Zeilen beider Tabellen"
|
|
]
|
|
],
|
|
"hint": "Denk an links, rechts und beide Seiten.",
|
|
"successText": "Alle JOIN-Typen aufgeklärt!"
|
|
},
|
|
{
|
|
"type": "spot",
|
|
"prompt": "Fall: Tippfehler im Schlüsselwort",
|
|
"points": 100,
|
|
"narrative": "Ein Schlüsselwort wurde vertippt und blockiert die Abfrage.",
|
|
"instruction": "Klick den fehlerhaften Token an.",
|
|
"tokens": [
|
|
"SELECT",
|
|
"COUNT(*)",
|
|
"FROM",
|
|
"mitarbeiter",
|
|
"GROPU BY",
|
|
"abteilung",
|
|
";"
|
|
],
|
|
"bugIndex": 4,
|
|
"fix": "GROUP BY",
|
|
"hint": "Lies das Gruppierungs-Keyword genau.",
|
|
"successText": "Tippfehler entlarvt: GROUP BY!"
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Fall: Liste von Werten",
|
|
"points": 100,
|
|
"narrative": "Gesucht sind Kunden aus Berlin oder Leipzig.",
|
|
"codeTemplate": "SELECT *\nFROM kunden\nWHERE stadt {{1}} ('Berlin', 'Leipzig');",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Operator",
|
|
"hint": "Prüft, ob ein Wert in einer Liste steht.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"IN"
|
|
]
|
|
}
|
|
],
|
|
"hint": "Zwei Buchstaben.",
|
|
"successText": "Beide Städte erfasst!"
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Fall: Kreuzprodukt",
|
|
"points": 100,
|
|
"question": "Wie viele Zeilen liefert ein CROSS JOIN von 3 und 4 Zeilen?",
|
|
"options": [
|
|
"7",
|
|
"12",
|
|
"3",
|
|
"4"
|
|
],
|
|
"correctIndex": 1,
|
|
"hint": "Beim Kreuzprodukt wird multipliziert.",
|
|
"successText": "Richtig, 3 mal 4 = 12 Zeilen."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Fall: Eindeutige zählen",
|
|
"points": 100,
|
|
"narrative": "Wie viele verschiedene Städte kommen vor?",
|
|
"codeTemplate": "SELECT COUNT({{1}} stadt)\nFROM kunden;",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Keyword",
|
|
"hint": "Zählt nur unterschiedliche Werte.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"DISTINCT"
|
|
]
|
|
}
|
|
],
|
|
"hint": "Das gleiche Wort, das Duplikate entfernt.",
|
|
"successText": "Nur eindeutige Städte gezählt!"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"slug": "debug-hunt",
|
|
"title": "Debug Hunt",
|
|
"language": "Java",
|
|
"path": "debugging",
|
|
"description": "Jage echte Bugs in Java-Code: falsche Typen, Vergleiche, Indizes und mehr.",
|
|
"tasks": [
|
|
{
|
|
"type": "spot",
|
|
"prompt": "Falscher Datentyp",
|
|
"points": 100,
|
|
"narrative": "Hier wird einer int-Variablen ein Text zugewiesen.",
|
|
"instruction": "Klick den fehlerhaften Token an.",
|
|
"tokens": [
|
|
"int",
|
|
"x",
|
|
"=",
|
|
"\"10\"",
|
|
";"
|
|
],
|
|
"bugIndex": 3,
|
|
"fix": "10",
|
|
"hint": "Ein int speichert Zahlen, keinen String.",
|
|
"successText": "Erwischt! 10 ohne Anführungszeichen."
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Was wird ausgegeben?",
|
|
"points": 100,
|
|
"question": "Was gibt System.out.println(5 + 3 + \"!\"); aus?",
|
|
"options": [
|
|
"53!",
|
|
"8!",
|
|
"5 + 3!",
|
|
"!8"
|
|
],
|
|
"correctIndex": 1,
|
|
"hint": "Erst werden die Zahlen addiert, dann kommt der Text.",
|
|
"successText": "Richtig, 5+3=8, also 8!"
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Vergleich statt Zuweisung",
|
|
"points": 100,
|
|
"narrative": "In der Bedingung soll verglichen werden, nicht zugewiesen.",
|
|
"codeTemplate": "if (x {{1}} 5) {\n System.out.println(\"fünf\");\n}",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Operator",
|
|
"hint": "Doppeltes Zeichen für Gleichheit.",
|
|
"caseSensitive": true,
|
|
"solutions": [
|
|
"=="
|
|
]
|
|
}
|
|
],
|
|
"hint": "Ein einzelnes = würde nur zuweisen.",
|
|
"successText": "Korrekt, == vergleicht Werte."
|
|
},
|
|
{
|
|
"type": "spot",
|
|
"prompt": "Fehlendes Semikolon",
|
|
"points": 100,
|
|
"narrative": "Diese Zeile endet mit dem falschen Zeichen.",
|
|
"instruction": "Klick den fehlerhaften Token an.",
|
|
"tokens": [
|
|
"int",
|
|
"y",
|
|
"=",
|
|
"5",
|
|
","
|
|
],
|
|
"bugIndex": 4,
|
|
"fix": ";",
|
|
"hint": "Anweisungen enden in Java nicht mit Komma.",
|
|
"successText": "Gut, eine Anweisung endet mit ;"
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Null-Prüfung",
|
|
"points": 100,
|
|
"narrative": "Vor dem Zugriff soll geprüft werden, ob name nicht null ist.",
|
|
"codeTemplate": "if (name {{1}} null) {\n name.length();\n}",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Operator",
|
|
"hint": "Ungleich.",
|
|
"caseSensitive": true,
|
|
"solutions": [
|
|
"!="
|
|
]
|
|
}
|
|
],
|
|
"hint": "Ausrufezeichen und Gleichheitszeichen.",
|
|
"successText": "Sicher, != null verhindert den Crash."
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "String-Vergleich",
|
|
"points": 100,
|
|
"question": "Welcher Fehler steckt in if (s == \"hallo\") für einen Inhaltsvergleich?",
|
|
"options": [
|
|
"Vergleicht Referenzen statt Inhalt",
|
|
"Syntaxfehler",
|
|
"Nichts, ist korrekt",
|
|
"s muss großgeschrieben sein"
|
|
],
|
|
"correctIndex": 0,
|
|
"hint": "Für String-Inhalte nutzt man equals().",
|
|
"successText": "Richtig, == vergleicht nur Referenzen."
|
|
},
|
|
{
|
|
"type": "spot",
|
|
"prompt": "Index außerhalb der Grenzen",
|
|
"points": 100,
|
|
"narrative": "Diese Schleife greift einen Schritt zu weit zu.",
|
|
"instruction": "Klick den fehlerhaften Token an.",
|
|
"tokens": [
|
|
"for",
|
|
"(",
|
|
"int",
|
|
"i",
|
|
"=",
|
|
"0",
|
|
";",
|
|
"i",
|
|
"<=",
|
|
"arr.length",
|
|
";",
|
|
"i++",
|
|
")"
|
|
],
|
|
"bugIndex": 8,
|
|
"fix": "<",
|
|
"hint": "Gültige Indizes gehen von 0 bis length-1.",
|
|
"successText": "Gelöst! i < arr.length verhindert den Überlauf."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Rückgabewert",
|
|
"points": 100,
|
|
"narrative": "Die Methode soll das Quadrat zurückgeben, tut es aber nicht.",
|
|
"codeTemplate": "int quadrat(int n) {\n {{1}} n * n;\n}",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Keyword",
|
|
"hint": "Liefert einen Wert zurück.",
|
|
"caseSensitive": true,
|
|
"solutions": [
|
|
"return"
|
|
]
|
|
}
|
|
],
|
|
"hint": "Sechs Buchstaben, kleingeschrieben.",
|
|
"successText": "Korrekt, return gibt das Ergebnis zurück."
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Ganzzahl-Division",
|
|
"points": 100,
|
|
"question": "Was ergibt 10 / 3 in Java, wenn beide Werte int sind?",
|
|
"options": [
|
|
"3.33",
|
|
"3",
|
|
"4",
|
|
"3.0"
|
|
],
|
|
"correctIndex": 1,
|
|
"hint": "Bei int-Division wird der Rest verworfen.",
|
|
"successText": "Richtig, das Ergebnis ist 3."
|
|
},
|
|
{
|
|
"type": "spot",
|
|
"prompt": "Falscher Variablentyp",
|
|
"points": 100,
|
|
"narrative": "Ein Wahrheitswert wird im falschen Typ gespeichert.",
|
|
"instruction": "Klick den fehlerhaften Token an.",
|
|
"tokens": [
|
|
"int",
|
|
"flag",
|
|
"=",
|
|
"true",
|
|
";"
|
|
],
|
|
"bugIndex": 0,
|
|
"fix": "boolean",
|
|
"hint": "true und false brauchen einen eigenen Typ.",
|
|
"successText": "Erwischt! boolean statt int."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Typ für Wahrheitswerte",
|
|
"points": 100,
|
|
"narrative": "Welcher Datentyp speichert true oder false?",
|
|
"codeTemplate": "{{1}} aktiv = true;",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Datentyp",
|
|
"hint": "Für ja/nein-Werte.",
|
|
"caseSensitive": true,
|
|
"solutions": [
|
|
"boolean"
|
|
]
|
|
}
|
|
],
|
|
"hint": "Sieben Buchstaben, kleingeschrieben.",
|
|
"successText": "Korrekt, boolean speichert Wahrheitswerte."
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "String-Verkettung",
|
|
"points": 100,
|
|
"question": "Was gibt System.out.println(\"\" + 5 + 3); aus?",
|
|
"options": [
|
|
"8",
|
|
"53",
|
|
"5 3",
|
|
"Fehler"
|
|
],
|
|
"correctIndex": 1,
|
|
"hint": "Sobald ein String dabei ist, wird verkettet statt addiert.",
|
|
"successText": "Richtig, das Ergebnis ist 53."
|
|
},
|
|
{
|
|
"type": "spot",
|
|
"prompt": "Verdrehter Operator",
|
|
"points": 100,
|
|
"narrative": "Der Ungleich-Operator wurde falsch herum geschrieben.",
|
|
"instruction": "Klick den fehlerhaften Token an.",
|
|
"tokens": [
|
|
"if",
|
|
"(",
|
|
"a",
|
|
"=!",
|
|
"b",
|
|
")"
|
|
],
|
|
"bugIndex": 3,
|
|
"fix": "!=",
|
|
"hint": "Erst das Ausrufezeichen, dann das Gleichheitszeichen.",
|
|
"successText": "Gelöst! Der Operator heißt !=."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Kurzschreibweise zum Addieren",
|
|
"points": 100,
|
|
"narrative": "Der Wert von summe soll um 5 erhöht werden.",
|
|
"codeTemplate": "int summe = 0;\nsumme {{1}} 5;",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Operator",
|
|
"hint": "Kurzform für summe = summe + 5.",
|
|
"caseSensitive": true,
|
|
"solutions": [
|
|
"+="
|
|
]
|
|
}
|
|
],
|
|
"hint": "Plus und Gleichheitszeichen.",
|
|
"successText": "Korrekt, += addiert und weist zu."
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Array-Zugriff",
|
|
"points": 100,
|
|
"question": "Was passiert bei int[] a = new int[3]; a[3] = 1;?",
|
|
"options": [
|
|
"Läuft normal durch",
|
|
"ArrayIndexOutOfBoundsException",
|
|
"NullPointerException",
|
|
"Kompilierfehler"
|
|
],
|
|
"correctIndex": 1,
|
|
"hint": "Gültige Indizes sind 0, 1 und 2.",
|
|
"successText": "Richtig, Index 3 existiert nicht."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Schleifenbedingung",
|
|
"points": 100,
|
|
"narrative": "Die Schleife soll genau zehnmal laufen (0 bis 9).",
|
|
"codeTemplate": "for (int i = 0; i {{1}} 10; i++) {\n System.out.println(i);\n}",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Operator",
|
|
"hint": "Kleiner als, nicht kleiner-gleich.",
|
|
"caseSensitive": true,
|
|
"solutions": [
|
|
"<"
|
|
]
|
|
}
|
|
],
|
|
"hint": "Mit <= würde sie elfmal laufen.",
|
|
"successText": "Korrekt, i < 10 ergibt 10 Durchläufe."
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Was steht in b?",
|
|
"points": 100,
|
|
"question": "Welchen Wert hat b nach boolean b = 5 > 3;?",
|
|
"options": [
|
|
"true",
|
|
"false",
|
|
"1",
|
|
"Fehler"
|
|
],
|
|
"correctIndex": 0,
|
|
"hint": "Ist 5 wirklich größer als 3?",
|
|
"successText": "Richtig, 5 > 3 ist true."
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"slug": "git-mission",
|
|
"title": "Git Mission",
|
|
"language": "Git",
|
|
"path": "debugging",
|
|
"description": "Meistere typische Repo-Situationen mit den richtigen Git-Befehlen und Reihenfolgen.",
|
|
"tasks": [
|
|
{
|
|
"type": "order",
|
|
"prompt": "Mission: Änderung veröffentlichen",
|
|
"points": 100,
|
|
"instruction": "Bring die Befehle in die richtige Reihenfolge.",
|
|
"items": [
|
|
"git add .",
|
|
"git commit -m \"feat: Login hinzugefügt\"",
|
|
"git push"
|
|
],
|
|
"hint": "Erst vormerken, dann festschreiben, dann hochladen.",
|
|
"successText": "Deine Änderung ist veröffentlicht!"
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Befehl: Festschreiben",
|
|
"points": 100,
|
|
"narrative": "Deine vorgemerkten Änderungen sollen gespeichert werden.",
|
|
"codeTemplate": "git {{1}} -m \"fix: Bug behoben\"",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Befehl",
|
|
"hint": "Schreibt Änderungen in die Historie.",
|
|
"caseSensitive": true,
|
|
"solutions": [
|
|
"commit"
|
|
]
|
|
}
|
|
],
|
|
"hint": "Sechs Buchstaben.",
|
|
"successText": "Commit erstellt!"
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Mission: Historie ansehen",
|
|
"points": 100,
|
|
"question": "Welcher Befehl zeigt den Commit-Verlauf?",
|
|
"options": [
|
|
"git status",
|
|
"git log",
|
|
"git diff",
|
|
"git show"
|
|
],
|
|
"correctIndex": 1,
|
|
"hint": "Englisch für Protokoll.",
|
|
"successText": "Richtig, git log zeigt die Historie."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Befehl: Branch wechseln",
|
|
"points": 100,
|
|
"narrative": "Du willst zurück auf den main-Branch.",
|
|
"codeTemplate": "git {{1}} main",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Befehl",
|
|
"hint": "Moderner Befehl zum Branch-Wechsel.",
|
|
"caseSensitive": true,
|
|
"solutions": [
|
|
"switch",
|
|
"checkout"
|
|
]
|
|
}
|
|
],
|
|
"hint": "switch oder das ältere checkout.",
|
|
"successText": "Du bist jetzt auf main!"
|
|
},
|
|
{
|
|
"type": "order",
|
|
"prompt": "Mission: Arbeit zwischenparken",
|
|
"points": 100,
|
|
"instruction": "Du musst schnell den Branch wechseln, ohne zu committen. Ordne die Schritte.",
|
|
"items": [
|
|
"git stash",
|
|
"git switch main",
|
|
"git pull",
|
|
"git switch feature",
|
|
"git stash pop"
|
|
],
|
|
"hint": "Erst sichern, Branch-Arbeit erledigen, dann zurückholen.",
|
|
"successText": "Deine Arbeit ist wieder da!"
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Mission: Lokale Änderung verwerfen",
|
|
"points": 100,
|
|
"question": "Du willst eine noch NICHT committete Änderung an datei.txt verwerfen. Welcher Befehl?",
|
|
"options": [
|
|
"git restore datei.txt",
|
|
"git revert HEAD",
|
|
"git reset --hard origin",
|
|
"git rm datei.txt"
|
|
],
|
|
"correctIndex": 0,
|
|
"hint": "restore stellt den letzten committeten Stand wieder her.",
|
|
"successText": "Richtig, git restore verwirft die Änderung."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Befehl: Branch zusammenführen",
|
|
"points": 100,
|
|
"narrative": "Der Feature-Branch soll in den aktuellen Branch übernommen werden.",
|
|
"codeTemplate": "git {{1}} feature/login",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Befehl",
|
|
"hint": "Führt zwei Branches zusammen.",
|
|
"caseSensitive": true,
|
|
"solutions": [
|
|
"merge"
|
|
]
|
|
}
|
|
],
|
|
"hint": "Englisch für verschmelzen.",
|
|
"successText": "Branches zusammengeführt!"
|
|
},
|
|
{
|
|
"type": "order",
|
|
"prompt": "Mission: Neuer Feature-Branch",
|
|
"points": 100,
|
|
"instruction": "Starte ein neues Feature und veröffentliche es. Ordne die Schritte.",
|
|
"items": [
|
|
"git switch -c feature/login",
|
|
"git add .",
|
|
"git commit -m \"Login umgesetzt\"",
|
|
"git push -u origin feature/login"
|
|
],
|
|
"hint": "Branch anlegen, vormerken, committen, hochladen.",
|
|
"successText": "Feature-Branch steht online!"
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Befehl: Einzelnen Commit holen",
|
|
"points": 100,
|
|
"narrative": "Du brauchst genau einen Commit aus einem anderen Branch.",
|
|
"codeTemplate": "git {{1}} a1b2c3d",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Befehl",
|
|
"hint": "Pflückt einen einzelnen Commit.",
|
|
"caseSensitive": true,
|
|
"solutions": [
|
|
"cherry-pick"
|
|
]
|
|
}
|
|
],
|
|
"hint": "Zwei Wörter mit Bindestrich.",
|
|
"successText": "Commit übernommen!"
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Mission: Gepushten Commit zurücknehmen",
|
|
"points": 100,
|
|
"question": "Welcher Befehl macht einen bereits gepushten Commit mit einem neuen Commit rückgängig?",
|
|
"options": [
|
|
"git reset --hard",
|
|
"git revert <hash>",
|
|
"git checkout",
|
|
"git clean"
|
|
],
|
|
"correctIndex": 1,
|
|
"hint": "Er ändert die Historie nicht, sondern kehrt sie um.",
|
|
"successText": "Richtig, git revert ist sicher für geteilte Branches."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Befehl: Commit umkehren",
|
|
"points": 100,
|
|
"narrative": "Der letzte Commit soll durch einen Gegen-Commit rückgängig gemacht werden.",
|
|
"codeTemplate": "git {{1}} HEAD",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Befehl",
|
|
"hint": "Erzeugt einen umkehrenden Commit.",
|
|
"caseSensitive": true,
|
|
"solutions": [
|
|
"revert"
|
|
]
|
|
}
|
|
],
|
|
"hint": "Englisch für rückgängig machen.",
|
|
"successText": "Der Commit ist umgekehrt!"
|
|
},
|
|
{
|
|
"type": "order",
|
|
"prompt": "Mission: Merge-Konflikt lösen",
|
|
"points": 100,
|
|
"instruction": "Beim Zusammenführen entsteht ein Konflikt. Ordne die Schritte zur Lösung.",
|
|
"items": [
|
|
"git merge feature",
|
|
"git status",
|
|
"git add konflikt.txt",
|
|
"git commit"
|
|
],
|
|
"hint": "Mergen, Konflikt prüfen, gelöste Datei vormerken, abschließen.",
|
|
"successText": "Konflikt gelöst und gemergt!"
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Befehl: Arbeit zwischenspeichern",
|
|
"points": 100,
|
|
"narrative": "Du willst deine Änderungen kurz beiseitelegen, ohne zu committen.",
|
|
"codeTemplate": "git {{1}}",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Befehl",
|
|
"hint": "Legt Änderungen auf einen Stapel.",
|
|
"caseSensitive": true,
|
|
"solutions": [
|
|
"stash"
|
|
]
|
|
}
|
|
],
|
|
"hint": "Englisch für Versteck.",
|
|
"successText": "Änderungen sicher zwischengespeichert!"
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Mission: Remote-Stand holen",
|
|
"points": 100,
|
|
"question": "Welcher Befehl lädt Änderungen vom Remote UND führt sie zusammen?",
|
|
"options": [
|
|
"git fetch",
|
|
"git pull",
|
|
"git push",
|
|
"git clone"
|
|
],
|
|
"correctIndex": 1,
|
|
"hint": "fetch holt nur, dieser Befehl mergt zusätzlich.",
|
|
"successText": "Richtig, git pull holt und mergt."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Befehl: Stash zurückholen",
|
|
"points": 100,
|
|
"narrative": "Deine zwischengespeicherten Änderungen sollen zurück in den Arbeitsbereich.",
|
|
"codeTemplate": "git stash {{1}}",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Befehl",
|
|
"hint": "Holt den letzten Stash und entfernt ihn vom Stapel.",
|
|
"caseSensitive": true,
|
|
"solutions": [
|
|
"pop"
|
|
]
|
|
}
|
|
],
|
|
"hint": "Drei Buchstaben.",
|
|
"successText": "Deine Änderungen sind zurück!"
|
|
},
|
|
{
|
|
"type": "order",
|
|
"prompt": "Mission: Gepushten Commit rückgängig",
|
|
"points": 100,
|
|
"instruction": "Ein fehlerhafter Commit ist schon auf dem Remote. Ordne die sichere Lösung.",
|
|
"items": [
|
|
"git log --oneline",
|
|
"git revert a1b2c3d",
|
|
"git push"
|
|
],
|
|
"hint": "Hash finden, umkehren, hochladen.",
|
|
"successText": "Der fehlerhafte Commit ist neutralisiert!"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"slug": "html-architect",
|
|
"title": "HTML Architect",
|
|
"language": "HTML",
|
|
"path": "syntax",
|
|
"description": "Baue Seiten mit semantischem HTML: Struktur-Elemente, Attribute und sauberes Markup.",
|
|
"tasks": [
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Kopfbereich auszeichnen",
|
|
"points": 100,
|
|
"narrative": "Oben auf der Seite stehen Logo und Titel. Welches Element umschliesst diesen Kopfbereich?",
|
|
"codeTemplate": "<{{1}}>\n <h1>Mindflux</h1>\n</{{2}}>",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Tag",
|
|
"hint": "Semantischer Kopfbereich.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"header"
|
|
]
|
|
},
|
|
{
|
|
"position": 2,
|
|
"placeholder": "Tag",
|
|
"hint": "Gleiches Tag wie das oeffnende.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"header"
|
|
]
|
|
}
|
|
],
|
|
"successText": "Stark! <header> markiert den einleitenden Bereich."
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Hauptinhalt",
|
|
"points": 100,
|
|
"question": "Welches Element kennzeichnet den zentralen Hauptinhalt eines Dokuments?",
|
|
"options": [
|
|
"<main>",
|
|
"<content>",
|
|
"<body>",
|
|
"<section id=\"main\">"
|
|
],
|
|
"correctIndex": 0,
|
|
"hint": "Es darf pro Seite nur einmal sichtbar vorkommen.",
|
|
"successText": "Genau, <main> umschliesst den Hauptinhalt."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Navigation",
|
|
"points": 100,
|
|
"narrative": "Die Linkliste fuehrt durch die Seite.",
|
|
"codeTemplate": "<{{1}} aria-label=\"Hauptmenue\">\n <a href=\"/\">Start</a>\n</{{2}}>",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Tag",
|
|
"hint": "Steht fuer Navigation.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"nav"
|
|
]
|
|
},
|
|
{
|
|
"position": 2,
|
|
"placeholder": "Tag",
|
|
"hint": "Gleiches Tag schliessen.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"nav"
|
|
]
|
|
}
|
|
],
|
|
"successText": "<nav> ist die richtige Wahl fuer Navigationsbloecke."
|
|
},
|
|
{
|
|
"type": "order",
|
|
"prompt": "Seitengeruest ordnen",
|
|
"points": 100,
|
|
"instruction": "Bringe die Grundbausteine einer Seite in die uebliche Reihenfolge von oben nach unten.",
|
|
"items": [
|
|
"<header>",
|
|
"<nav>",
|
|
"<main>",
|
|
"<footer>"
|
|
],
|
|
"hint": "Kopf, dann Menue, dann Inhalt, dann Fuss.",
|
|
"successText": "Sauberes Dokument-Outline!"
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Eigenstaendiger Beitrag",
|
|
"points": 100,
|
|
"narrative": "Ein Blogpost soll auch einzeln (z. B. im RSS-Feed) sinnvoll sein.",
|
|
"codeTemplate": "<{{1}}>\n <h2>Blogpost</h2>\n <p>Text</p>\n</{{2}}>",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Tag",
|
|
"hint": "Eigenstaendiger, wiederverwendbarer Inhalt.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"article"
|
|
]
|
|
},
|
|
{
|
|
"position": 2,
|
|
"placeholder": "Tag",
|
|
"hint": "Gleiches Tag schliessen.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"article"
|
|
]
|
|
}
|
|
],
|
|
"successText": "<article> kapselt einen in sich geschlossenen Beitrag."
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Navigationsbereich",
|
|
"points": 100,
|
|
"question": "Womit zeichnest du eine Navigationsleiste semantisch korrekt aus?",
|
|
"options": [
|
|
"<nav>",
|
|
"<div class=\"nav\">",
|
|
"<navigation>",
|
|
"<menu role=\"nav\">"
|
|
],
|
|
"correctIndex": 0,
|
|
"hint": "Es gibt ein dediziertes HTML-Element dafuer.",
|
|
"successText": "Richtig, <nav> ist standardisiert und barrierearm."
|
|
},
|
|
{
|
|
"type": "spot",
|
|
"prompt": "Falsches schliessendes Tag",
|
|
"points": 100,
|
|
"narrative": "Ein schliessendes Tag passt nicht zum oeffnenden Element.",
|
|
"instruction": "Klick den fehlerhaften Token an.",
|
|
"tokens": [
|
|
"<section>",
|
|
"Inhalt",
|
|
"</div>"
|
|
],
|
|
"bugIndex": 2,
|
|
"fix": "</section>",
|
|
"hint": "Welches Tag wurde geoeffnet?",
|
|
"successText": "Korrekt, oeffnendes und schliessendes Tag muessen uebereinstimmen."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Thematischer Abschnitt",
|
|
"points": 100,
|
|
"narrative": "Ein klar abgegrenztes Kapitel mit eigener Ueberschrift.",
|
|
"codeTemplate": "<{{1}}>\n <h2>Kapitel 1</h2>\n</{{2}}>",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Tag",
|
|
"hint": "Thematischer Block mit Ueberschrift.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"section"
|
|
]
|
|
},
|
|
{
|
|
"position": 2,
|
|
"placeholder": "Tag",
|
|
"hint": "Gleiches Tag schliessen.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"section"
|
|
]
|
|
}
|
|
],
|
|
"successText": "<section> gruppiert thematisch zusammengehoerigen Inhalt."
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Randinhalt",
|
|
"points": 100,
|
|
"question": "Welches Element eignet sich fuer ergaenzende Randinhalte wie eine Sidebar?",
|
|
"options": [
|
|
"<aside>",
|
|
"<side>",
|
|
"<extra>",
|
|
"<div class=\"sidebar\">"
|
|
],
|
|
"correctIndex": 0,
|
|
"hint": "Inhalt, der nur indirekt zum Hauptinhalt gehoert.",
|
|
"successText": "Genau, <aside> ist fuer nebensaechliche Inhalte gedacht."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Label verknuepfen",
|
|
"points": 100,
|
|
"narrative": "Das Label soll beim Klick das passende Eingabefeld fokussieren.",
|
|
"codeTemplate": "<label {{1}}=\"mail\">E-Mail</label>\n<input id=\"mail\" {{2}}=\"email\">",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Attribut",
|
|
"hint": "Verweist auf die id des Inputs.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"for"
|
|
]
|
|
},
|
|
{
|
|
"position": 2,
|
|
"placeholder": "Attribut",
|
|
"hint": "Bestimmt die Art des Eingabefelds.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"type"
|
|
]
|
|
}
|
|
],
|
|
"successText": "for verknuepft Label und Feld, type setzt die Eingabeart."
|
|
},
|
|
{
|
|
"type": "spot",
|
|
"prompt": "Falscher Input-Typ",
|
|
"points": 100,
|
|
"narrative": "Im type-Wert hat sich ein Tippfehler eingeschlichen.",
|
|
"instruction": "Klick den fehlerhaften Token an.",
|
|
"tokens": [
|
|
"<input",
|
|
"type=",
|
|
"\"emial\"",
|
|
">"
|
|
],
|
|
"bugIndex": 2,
|
|
"fix": "\"email\"",
|
|
"hint": "Lies den Typ-Wert genau.",
|
|
"successText": "type=\"email\" aktiviert die E-Mail-Validierung."
|
|
},
|
|
{
|
|
"type": "order",
|
|
"prompt": "Ueberschriften-Hierarchie",
|
|
"points": 100,
|
|
"instruction": "Ordne die Ueberschriften von der wichtigsten zur untergeordneten Ebene.",
|
|
"items": [
|
|
"<h1>",
|
|
"<h2>",
|
|
"<h3>",
|
|
"<h4>"
|
|
],
|
|
"hint": "h1 ist die oberste Ebene.",
|
|
"successText": "Eine saubere Hierarchie hilft Screenreadern und SEO."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Fussbereich",
|
|
"points": 100,
|
|
"narrative": "Ganz unten stehen Copyright und Kontakt.",
|
|
"codeTemplate": "<{{1}}>\n <p>(c) 2026 Mindflux</p>\n</{{2}}>",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Tag",
|
|
"hint": "Abschliessender Bereich.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"footer"
|
|
]
|
|
},
|
|
{
|
|
"position": 2,
|
|
"placeholder": "Tag",
|
|
"hint": "Gleiches Tag schliessen.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"footer"
|
|
]
|
|
}
|
|
],
|
|
"successText": "<footer> markiert den abschliessenden Bereich."
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Bild mit Beschriftung",
|
|
"points": 100,
|
|
"question": "Welche Kombination beschreibt ein Bild mit Bildunterschrift semantisch korrekt?",
|
|
"options": [
|
|
"<figure> + <figcaption>",
|
|
"<img> + <caption>",
|
|
"<picture> + <legend>",
|
|
"<div> + <small>"
|
|
],
|
|
"correctIndex": 0,
|
|
"hint": "Eines kapselt das Bild, das andere die Beschriftung.",
|
|
"successText": "Richtig, <figcaption> gehoert in eine <figure>."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Schaltflaeche",
|
|
"points": 100,
|
|
"narrative": "Der Button soll ein Formular absenden.",
|
|
"codeTemplate": "<button {{1}}=\"submit\">Senden</button>",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Attribut",
|
|
"hint": "Bestimmt das Verhalten des Buttons.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"type"
|
|
]
|
|
}
|
|
],
|
|
"successText": "type=\"submit\" loest das Absenden aus."
|
|
},
|
|
{
|
|
"type": "spot",
|
|
"prompt": "Nicht-semantischer Container",
|
|
"points": 100,
|
|
"narrative": "Eine Navigation sollte nicht als generischer Container ausgezeichnet sein.",
|
|
"instruction": "Klick den fehlerhaften Token an.",
|
|
"tokens": [
|
|
"<div",
|
|
"class=",
|
|
"\"nav\"",
|
|
">"
|
|
],
|
|
"bugIndex": 0,
|
|
"fix": "<nav",
|
|
"hint": "Welches dedizierte Element gibt es fuer Navigation?",
|
|
"successText": "Mit <nav> brauchst du keine Hilfsklasse fuer die Bedeutung."
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Ueberschriften-Ebene",
|
|
"points": 100,
|
|
"question": "Wie viele <h1>-Hauptueberschriften sind pro Seite ueblicherweise empfohlen?",
|
|
"options": [
|
|
"Genau eine",
|
|
"Beliebig viele",
|
|
"Mindestens drei",
|
|
"Keine"
|
|
],
|
|
"correctIndex": 0,
|
|
"hint": "Eine klare Hauptueberschrift pro Dokument.",
|
|
"successText": "Eine einzige h1 haelt die Struktur eindeutig."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Seiteninhalt umschliessen",
|
|
"points": 100,
|
|
"narrative": "Zwischen Header und Footer steht der zentrale Inhaltsbereich.",
|
|
"codeTemplate": "<{{1}}>\n <h1>Willkommen</h1>\n</{{2}}>",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Tag",
|
|
"hint": "Der eine zentrale Hauptinhalt.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"main"
|
|
]
|
|
},
|
|
{
|
|
"position": 2,
|
|
"placeholder": "Tag",
|
|
"hint": "Gleiches Tag schliessen.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"main"
|
|
]
|
|
}
|
|
],
|
|
"successText": "<main> umschliesst den dominanten Inhalt der Seite."
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"slug": "css-designer",
|
|
"title": "CSS Designer",
|
|
"language": "CSS",
|
|
"path": "visual",
|
|
"description": "Layout mit Flexbox und Grid: display, gap, Ausrichtung und Responsive-Grundlagen.",
|
|
"tasks": [
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Flex-Container aktivieren",
|
|
"points": 100,
|
|
"narrative": "Die Buttons sollen in einer Reihe nebeneinander liegen.",
|
|
"codeTemplate": ".bar {\n {{1}}: {{2}};\n}",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Property",
|
|
"hint": "Bestimmt das Layout-Modell.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"display"
|
|
]
|
|
},
|
|
{
|
|
"position": 2,
|
|
"placeholder": "Wert",
|
|
"hint": "Eindimensionales Reihen-Layout.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"flex"
|
|
]
|
|
}
|
|
],
|
|
"successText": "display: flex macht das Element zum Flex-Container."
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Horizontal verteilen",
|
|
"points": 100,
|
|
"question": "Mit welcher Property verteilst du Flex-Items entlang der Hauptachse?",
|
|
"options": [
|
|
"justify-content",
|
|
"align-items",
|
|
"text-align",
|
|
"float"
|
|
],
|
|
"correctIndex": 0,
|
|
"hint": "Haupt- vs. Querachse beachten.",
|
|
"successText": "justify-content steuert die Verteilung auf der Hauptachse."
|
|
},
|
|
{
|
|
"type": "spot",
|
|
"prompt": "Falscher Anzeige-Wert",
|
|
"points": 100,
|
|
"narrative": "Der Container soll ein Flexbox-Layout nutzen, aber der Wert stimmt nicht.",
|
|
"instruction": "Klick den fehlerhaften Token an.",
|
|
"tokens": [
|
|
"display",
|
|
":",
|
|
"flexbox",
|
|
";"
|
|
],
|
|
"bugIndex": 2,
|
|
"fix": "flex",
|
|
"hint": "Der CSS-Wert heisst nicht wie das Konzept.",
|
|
"successText": "Korrekt, der Wert lautet flex, nicht flexbox."
|
|
},
|
|
{
|
|
"type": "match",
|
|
"prompt": "Layout-Properties zuordnen",
|
|
"points": 100,
|
|
"instruction": "Ordne jede Property ihrer Wirkung zu.",
|
|
"pairs": [
|
|
[
|
|
"display: flex",
|
|
"Flex-Layout aktivieren"
|
|
],
|
|
[
|
|
"gap",
|
|
"Abstand zwischen Items"
|
|
],
|
|
[
|
|
"justify-content",
|
|
"Ausrichtung Hauptachse"
|
|
],
|
|
[
|
|
"align-items",
|
|
"Ausrichtung Querachse"
|
|
]
|
|
],
|
|
"hint": "Achte auf Haupt- und Querachse.",
|
|
"successText": "Perfekt zugeordnet!"
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Abstand zwischen Items",
|
|
"points": 100,
|
|
"narrative": "Zwischen den Kacheln soll ein gleichmaessiger Abstand entstehen.",
|
|
"codeTemplate": ".grid {\n display: grid;\n {{1}}: 16px;\n}",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Property",
|
|
"hint": "Funktioniert in Grid und Flex.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"gap"
|
|
]
|
|
}
|
|
],
|
|
"successText": "gap erzeugt Abstaende ohne Rand-Tricks."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Zentriert verteilen",
|
|
"points": 100,
|
|
"narrative": "Die Items sollen mittig auf der Hauptachse sitzen.",
|
|
"codeTemplate": ".row {\n display: flex;\n justify-content: {{1}};\n}",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Wert",
|
|
"hint": "Mittige Ausrichtung.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"center"
|
|
]
|
|
}
|
|
],
|
|
"successText": "center buendelt die Items in der Mitte."
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Zweidimensionales Layout",
|
|
"points": 100,
|
|
"question": "Welches Modell eignet sich am besten fuer ein Raster aus Zeilen UND Spalten?",
|
|
"options": [
|
|
"display: grid",
|
|
"display: flex",
|
|
"display: block",
|
|
"display: inline"
|
|
],
|
|
"correctIndex": 0,
|
|
"hint": "Flex ist eindimensional, das gesuchte ist zweidimensional.",
|
|
"successText": "Grid ist fuer zweidimensionale Layouts gemacht."
|
|
},
|
|
{
|
|
"type": "spot",
|
|
"prompt": "Tippfehler im Wert",
|
|
"points": 100,
|
|
"narrative": "Der Verteilungs-Wert ist falsch geschrieben.",
|
|
"instruction": "Klick den fehlerhaften Token an.",
|
|
"tokens": [
|
|
"justify-content",
|
|
":",
|
|
"space-betwen",
|
|
";"
|
|
],
|
|
"bugIndex": 2,
|
|
"fix": "space-between",
|
|
"hint": "Da fehlt ein Buchstabe.",
|
|
"successText": "Richtig, der Wert heisst space-between."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Spalten definieren",
|
|
"points": 100,
|
|
"narrative": "Das Grid soll drei gleich breite Spalten bekommen.",
|
|
"codeTemplate": ".grid {\n display: grid;\n {{1}}: 1fr 1fr 1fr;\n}",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Property",
|
|
"hint": "Definiert die Spaltenspur.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"grid-template-columns"
|
|
]
|
|
}
|
|
],
|
|
"successText": "grid-template-columns legt Anzahl und Breite der Spalten fest."
|
|
},
|
|
{
|
|
"type": "match",
|
|
"prompt": "justify-content Werte",
|
|
"points": 100,
|
|
"instruction": "Ordne jeden Wert seiner Wirkung zu.",
|
|
"pairs": [
|
|
[
|
|
"center",
|
|
"Items mittig zusammen"
|
|
],
|
|
[
|
|
"space-between",
|
|
"Erstes/letztes aussen, Rest verteilt"
|
|
],
|
|
[
|
|
"flex-start",
|
|
"Items am Anfang"
|
|
],
|
|
[
|
|
"space-around",
|
|
"Gleicher Raum um jedes Item"
|
|
]
|
|
],
|
|
"hint": "Denk an die Position des ersten und letzten Items.",
|
|
"successText": "Alle Werte richtig erkannt!"
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Querachse ausrichten",
|
|
"points": 100,
|
|
"narrative": "Die Items sollen vertikal mittig in der Reihe stehen.",
|
|
"codeTemplate": ".row {\n display: flex;\n {{1}}: center;\n}",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Property",
|
|
"hint": "Wirkt auf der Querachse.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"align-items"
|
|
]
|
|
}
|
|
],
|
|
"successText": "align-items richtet Items auf der Querachse aus."
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Responsives Verhalten",
|
|
"points": 100,
|
|
"question": "Womit passt du Styles ab einer bestimmten Bildschirmbreite an?",
|
|
"options": [
|
|
"@media (min-width: 600px) { ... }",
|
|
"@responsive 600px { ... }",
|
|
"@screen > 600 { ... }",
|
|
"@width: 600px"
|
|
],
|
|
"correctIndex": 0,
|
|
"hint": "Es ist eine At-Regel mit Bedingung.",
|
|
"successText": "Media Queries reagieren auf die Viewport-Breite."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Eine flexible Spalte",
|
|
"points": 100,
|
|
"narrative": "Auf schmalen Schirmen soll alles in einer flexiblen Spalte ueber die volle Breite stehen.",
|
|
"codeTemplate": "@media (max-width: 600px) {\n .grid { grid-template-columns: {{1}}; }\n}",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Wert",
|
|
"hint": "Eine flexible Einheit, die den ganzen Platz nimmt.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"1fr"
|
|
]
|
|
}
|
|
],
|
|
"successText": "1fr fuellt den verfuegbaren Raum mit einer Spalte."
|
|
},
|
|
{
|
|
"type": "spot",
|
|
"prompt": "Tippfehler in der Property",
|
|
"points": 100,
|
|
"narrative": "Der Property-Name ist verdreht.",
|
|
"instruction": "Klick den fehlerhaften Token an.",
|
|
"tokens": [
|
|
"grid-template-colmuns",
|
|
":",
|
|
"1fr 1fr",
|
|
";"
|
|
],
|
|
"bugIndex": 0,
|
|
"fix": "grid-template-columns",
|
|
"hint": "Lies das Wort 'columns' genau.",
|
|
"successText": "Richtig geschrieben heisst es grid-template-columns."
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Luecke zwischen Zellen",
|
|
"points": 100,
|
|
"question": "Welche Property erzeugt gleichmaessige Abstaende zwischen Grid- oder Flex-Items?",
|
|
"options": [
|
|
"gap",
|
|
"margin-collapse",
|
|
"spacing",
|
|
"padding-between"
|
|
],
|
|
"correctIndex": 0,
|
|
"hint": "Funktioniert ohne Rand am letzten Element.",
|
|
"successText": "gap ist die moderne Loesung fuer Item-Abstaende."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Grid-Container aktivieren",
|
|
"points": 100,
|
|
"narrative": "Die Galerie soll als Raster angeordnet werden.",
|
|
"codeTemplate": ".gallery {\n {{1}}: {{2}};\n}",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Property",
|
|
"hint": "Bestimmt das Layout-Modell.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"display"
|
|
]
|
|
},
|
|
{
|
|
"position": 2,
|
|
"placeholder": "Wert",
|
|
"hint": "Zweidimensionales Raster.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"grid"
|
|
]
|
|
}
|
|
],
|
|
"successText": "display: grid eroeffnet das Raster-Layout."
|
|
},
|
|
{
|
|
"type": "match",
|
|
"prompt": "Property zu Effekt",
|
|
"points": 100,
|
|
"instruction": "Ordne die Property dem sichtbaren Effekt zu.",
|
|
"pairs": [
|
|
[
|
|
"flex-direction: column",
|
|
"Items untereinander"
|
|
],
|
|
[
|
|
"flex-wrap: wrap",
|
|
"Umbruch in neue Zeile"
|
|
],
|
|
[
|
|
"grid-template-columns",
|
|
"Spaltenanzahl/-breite"
|
|
],
|
|
[
|
|
"gap",
|
|
"Abstand zwischen Items"
|
|
]
|
|
],
|
|
"hint": "Stell dir das Layout bildlich vor.",
|
|
"successText": "Sehr gut, alle Effekte richtig zugeordnet!"
|
|
},
|
|
{
|
|
"type": "spot",
|
|
"prompt": "Tippfehler im Property-Namen",
|
|
"points": 100,
|
|
"narrative": "Der Name der Ausrichtungs-Property ist falsch geschrieben.",
|
|
"instruction": "Klick den fehlerhaften Token an.",
|
|
"tokens": [
|
|
"allign-items",
|
|
":",
|
|
"center",
|
|
";"
|
|
],
|
|
"bugIndex": 0,
|
|
"fix": "align-items",
|
|
"hint": "Wie viele L hat 'align'?",
|
|
"successText": "Korrekt, align-items wird mit einem L geschrieben."
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"slug": "css-cascade",
|
|
"title": "CSS Cascade",
|
|
"language": "CSS",
|
|
"path": "visual",
|
|
"description": "Verstehe Spezifitaet, Reihenfolge, Vererbung und !important - und welche Regel am Ende gewinnt.",
|
|
"tasks": [
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Wer gewinnt: ID oder Klasse?",
|
|
"points": 100,
|
|
"question": "Beide Regeln treffen dasselbe Element:\n#box { color: red; }\n.box { color: blue; }\nWelche setzt sich durch?",
|
|
"options": [
|
|
"#box (ID ist spezifischer)",
|
|
".box (Klasse ist spezifischer)",
|
|
"Die zuletzt geschriebene Regel",
|
|
"Keine, sie heben sich auf"
|
|
],
|
|
"correctIndex": 0,
|
|
"hint": "ID-Selektoren wiegen schwerer als Klassen.",
|
|
"successText": "Richtig, eine ID schlaegt eine Klasse."
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Inline gegen ID",
|
|
"points": 100,
|
|
"question": "Welche Deklaration hat die hoechste Spezifitaet (ohne !important)?",
|
|
"options": [
|
|
"Ein Inline-Style (style-Attribut)",
|
|
"Ein ID-Selektor",
|
|
"Ein Klassen-Selektor",
|
|
"Ein Element-Selektor"
|
|
],
|
|
"correctIndex": 0,
|
|
"hint": "Denke an die Spezifitaets-Stufen a,b,c,d.",
|
|
"successText": "Inline-Styles haben die hoechste normale Spezifitaet."
|
|
},
|
|
{
|
|
"type": "match",
|
|
"prompt": "Spezifitaet zuordnen",
|
|
"points": 100,
|
|
"instruction": "Ordne jeden Selektor seiner Spezifitaet (IDs, Klassen, Elemente) zu.",
|
|
"pairs": [
|
|
[
|
|
"#header",
|
|
"1,0,0"
|
|
],
|
|
[
|
|
".nav",
|
|
"0,1,0"
|
|
],
|
|
[
|
|
"a",
|
|
"0,0,1"
|
|
],
|
|
[
|
|
"#header .nav a",
|
|
"1,1,1"
|
|
]
|
|
],
|
|
"hint": "Zaehle IDs, dann Klassen, dann Element-Namen.",
|
|
"successText": "Perfekt, du zaehlst Spezifitaet richtig!"
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Spezifischer werden",
|
|
"points": 100,
|
|
"narrative": "Die Klasse allein verliert gegen eine andere Regel. Erhoehe die Spezifitaet, indem du die ID #card des Containers voranstellst.",
|
|
"codeTemplate": "{{1}} .title {\n color: var(--accent);\n}",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Selektor",
|
|
"hint": "ID-Selektor des Containers.",
|
|
"caseSensitive": true,
|
|
"solutions": [
|
|
"#card"
|
|
]
|
|
}
|
|
],
|
|
"successText": "Mit #card davor gewinnt die Regel an Gewicht."
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Notbremse !important",
|
|
"points": 100,
|
|
"question": "Welche Aussage ueber !important ist korrekt?",
|
|
"options": [
|
|
"Es ueberschreibt normale Deklarationen unabhaengig von der Spezifitaet",
|
|
"Es erhoeht die Spezifitaet um genau eine ID",
|
|
"Es wirkt nur bei Inline-Styles",
|
|
"Es deaktiviert die Vererbung"
|
|
],
|
|
"correctIndex": 0,
|
|
"hint": "!important steht ausserhalb der normalen Spezifitaets-Rechnung.",
|
|
"successText": "Genau, !important sticht normale Regeln - sparsam einsetzen."
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Gleiche Spezifitaet",
|
|
"points": 100,
|
|
"question": "Zwei Regeln gleicher Spezifitaet treffen dasselbe Element:\n.btn { color: red; }\n.btn { color: green; }\nWelche gewinnt?",
|
|
"options": [
|
|
"Die spaeter im Stylesheet stehende (green)",
|
|
"Die zuerst stehende (red)",
|
|
"Beide gemischt",
|
|
"Die kuerzere Regel"
|
|
],
|
|
"correctIndex": 0,
|
|
"hint": "Bei Gleichstand entscheidet die Quellreihenfolge.",
|
|
"successText": "Bei gleicher Spezifitaet gewinnt die letzte Regel."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Farbe als Token",
|
|
"points": 100,
|
|
"narrative": "Statt einer hartkodierten Hex-Farbe nutzt du eine CSS-Variable, damit Themes durchgreifen koennen.",
|
|
"codeTemplate": ".badge {\n color: {{1}};\n}",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Wert",
|
|
"hint": "Greift auf eine Custom-Property zu: var(--name).",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"var(--accent)"
|
|
]
|
|
}
|
|
],
|
|
"successText": "var(--accent) bleibt theme-faehig statt fest verdrahtet."
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Element gegen Klasse",
|
|
"points": 100,
|
|
"question": "p { color: black; } und .lead { color: gray; } treffen dasselbe Element. Welche gewinnt?",
|
|
"options": [
|
|
".lead (Klasse schlaegt Element)",
|
|
"p (Element schlaegt Klasse)",
|
|
"Die zuletzt geladene Datei",
|
|
"Keine"
|
|
],
|
|
"correctIndex": 0,
|
|
"hint": "Eine Klasse wiegt mehr als ein Element-Name.",
|
|
"successText": "Richtig, die Klasse .lead ist spezifischer."
|
|
},
|
|
{
|
|
"type": "match",
|
|
"prompt": "Kaskaden-Faktoren ordnen",
|
|
"points": 100,
|
|
"instruction": "Ordne jeden Faktor seiner Rolle in der Kaskade zu.",
|
|
"pairs": [
|
|
[
|
|
"Spezifitaet",
|
|
"Gewicht des Selektors"
|
|
],
|
|
[
|
|
"Reihenfolge",
|
|
"Spaeteres ueberschreibt Gleichwertiges"
|
|
],
|
|
[
|
|
"!important",
|
|
"Hebt normale Regeln auf"
|
|
],
|
|
[
|
|
"Vererbung",
|
|
"Werte fliessen an Kindelemente"
|
|
]
|
|
],
|
|
"hint": "Vier Saeulen entscheiden, welcher Wert greift.",
|
|
"successText": "Du kennst die Hebel der Kaskade!"
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Spezifitaet zaehlen",
|
|
"points": 100,
|
|
"question": "Welche Spezifitaet (IDs, Klassen, Elemente) hat der Selektor ul li a.active?",
|
|
"options": [
|
|
"0,1,3",
|
|
"0,2,2",
|
|
"1,1,2",
|
|
"0,1,2"
|
|
],
|
|
"correctIndex": 0,
|
|
"hint": "Drei Element-Namen und eine Klasse.",
|
|
"successText": "Genau: 0 IDs, 1 Klasse, 3 Elemente = 0,1,3."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Variable definieren",
|
|
"points": 100,
|
|
"narrative": "Lege das Theme-Token zentral an, damit alle Komponenten darauf zugreifen koennen.",
|
|
"codeTemplate": ":root {\n {{1}}: #4f46e5;\n}",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Custom-Property",
|
|
"hint": "Custom Properties beginnen mit zwei Bindestrichen.",
|
|
"caseSensitive": true,
|
|
"solutions": [
|
|
"--accent"
|
|
]
|
|
}
|
|
],
|
|
"successText": "--accent ist nun als wiederverwendbares Token verfuegbar."
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Vererbung",
|
|
"points": 100,
|
|
"question": "Welche Eigenschaft wird typischerweise an Kindelemente vererbt?",
|
|
"options": [
|
|
"color",
|
|
"margin",
|
|
"padding",
|
|
"border"
|
|
],
|
|
"correctIndex": 0,
|
|
"hint": "Text-bezogene Eigenschaften vererben sich oft.",
|
|
"successText": "Richtig, color wird standardmaessig vererbt."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Per ID treffen",
|
|
"points": 100,
|
|
"narrative": "Sprich genau das Element mit der ID main an.",
|
|
"codeTemplate": "{{1}} {\n padding: 24px;\n}",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Selektor",
|
|
"hint": "ID-Selektor beginnt mit einer Raute.",
|
|
"caseSensitive": true,
|
|
"solutions": [
|
|
"#main"
|
|
]
|
|
}
|
|
],
|
|
"successText": "#main spricht das Element ueber seine ID an."
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Universalselektor",
|
|
"points": 100,
|
|
"question": "Welche Spezifitaet (IDs, Klassen, Elemente) traegt der Universalselektor * ?",
|
|
"options": [
|
|
"0,0,0",
|
|
"0,0,1",
|
|
"1,0,0",
|
|
"0,1,0"
|
|
],
|
|
"correctIndex": 0,
|
|
"hint": "Der Stern hat kein eigenes Gewicht.",
|
|
"successText": "Korrekt, * traegt die Spezifitaet 0,0,0."
|
|
},
|
|
{
|
|
"type": "match",
|
|
"prompt": "Selektoren vergleichen",
|
|
"points": 100,
|
|
"instruction": "Ordne jeden Selektor seiner Spezifitaet zu.",
|
|
"pairs": [
|
|
[
|
|
"*",
|
|
"0,0,0"
|
|
],
|
|
[
|
|
"li",
|
|
"0,0,1"
|
|
],
|
|
[
|
|
".active",
|
|
"0,1,0"
|
|
],
|
|
[
|
|
"#main",
|
|
"1,0,0"
|
|
]
|
|
],
|
|
"hint": "Von kein Gewicht bis ID aufsteigend.",
|
|
"successText": "Du sortierst Spezifitaet sicher!"
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Wer schlaegt wen?",
|
|
"points": 100,
|
|
"question": "Eine Stylesheet-Regel hat !important, ein Inline-Style hat es nicht. Welche gewinnt?",
|
|
"options": [
|
|
"Die Regel mit !important",
|
|
"Der Inline-Style",
|
|
"Die mit der hoeheren Spezifitaet",
|
|
"Beide gleich"
|
|
],
|
|
"correctIndex": 0,
|
|
"hint": "!important steht ueber normalen Inline-Styles.",
|
|
"successText": "Richtig, !important schlaegt einen normalen Inline-Style."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Klasse an Element binden",
|
|
"points": 100,
|
|
"narrative": "Erhoehe die Spezifitaet, indem du die Klasse active direkt an das Element a bindest.",
|
|
"codeTemplate": "a{{1}} {\n color: var(--accent);\n}",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Selektor-Teil",
|
|
"hint": "Klassen-Selektor beginnt mit einem Punkt.",
|
|
"caseSensitive": true,
|
|
"solutions": [
|
|
".active"
|
|
]
|
|
}
|
|
],
|
|
"successText": "a.active ist spezifischer als a allein."
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"slug": "flexbox-factory",
|
|
"title": "Flexbox Factory",
|
|
"language": "CSS",
|
|
"path": "visual",
|
|
"description": "Baue Flexbox-Layouts Schritt fuer Schritt: Richtung, Ausrichtung, Umbruch und flexible Items.",
|
|
"tasks": [
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Flexbox starten",
|
|
"points": 100,
|
|
"narrative": "Erster Schritt jedes Flex-Layouts: den Container aktivieren.",
|
|
"codeTemplate": ".container {\n {{1}}: {{2}};\n}",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Property",
|
|
"hint": "Bestimmt das Layout-Modell.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"display"
|
|
]
|
|
},
|
|
{
|
|
"position": 2,
|
|
"placeholder": "Wert",
|
|
"hint": "Eindimensionales Flex-Layout.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"flex"
|
|
]
|
|
}
|
|
],
|
|
"successText": "display: flex macht alle direkten Kinder zu Flex-Items."
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Items untereinander",
|
|
"points": 100,
|
|
"question": "Welcher Wert von flex-direction stapelt die Items vertikal untereinander?",
|
|
"options": [
|
|
"column",
|
|
"row",
|
|
"stack",
|
|
"vertical"
|
|
],
|
|
"correctIndex": 0,
|
|
"hint": "Spalte statt Reihe.",
|
|
"successText": "column dreht die Hauptachse in die Vertikale."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Richtung festlegen",
|
|
"points": 100,
|
|
"narrative": "Das Menue soll seine Eintraege untereinander anzeigen.",
|
|
"codeTemplate": ".menu {\n display: flex;\n {{1}}: column;\n}",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Property",
|
|
"hint": "Bestimmt die Hauptachsen-Richtung.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"flex-direction"
|
|
]
|
|
}
|
|
],
|
|
"successText": "flex-direction steuert, wohin die Hauptachse zeigt."
|
|
},
|
|
{
|
|
"type": "match",
|
|
"prompt": "Flex-Properties zuordnen",
|
|
"points": 100,
|
|
"instruction": "Ordne jede Property ihrer Wirkung zu.",
|
|
"pairs": [
|
|
[
|
|
"flex-direction",
|
|
"Richtung der Hauptachse"
|
|
],
|
|
[
|
|
"justify-content",
|
|
"Verteilung auf der Hauptachse"
|
|
],
|
|
[
|
|
"align-items",
|
|
"Ausrichtung auf der Querachse"
|
|
],
|
|
[
|
|
"flex-wrap",
|
|
"Umbruch in mehrere Zeilen"
|
|
]
|
|
],
|
|
"hint": "Haupt- vs. Querachse im Kopf behalten.",
|
|
"successText": "Alle Flex-Properties sitzen!"
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Auf der Hauptachse verteilen",
|
|
"points": 100,
|
|
"narrative": "Logo links, Menue rechts - mit maximalem Abstand dazwischen.",
|
|
"codeTemplate": ".row {\n display: flex;\n {{1}}: space-between;\n}",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Property",
|
|
"hint": "Verteilt entlang der Hauptachse.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"justify-content"
|
|
]
|
|
}
|
|
],
|
|
"successText": "justify-content: space-between druckt die Items nach aussen."
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Hauptachse bei row",
|
|
"points": 100,
|
|
"question": "Bei flex-direction: row - in welche Richtung verlaeuft die Hauptachse?",
|
|
"options": [
|
|
"Horizontal (links nach rechts)",
|
|
"Vertikal (oben nach unten)",
|
|
"Diagonal",
|
|
"Es gibt keine Hauptachse"
|
|
],
|
|
"correctIndex": 0,
|
|
"hint": "row bedeutet Reihe.",
|
|
"successText": "Genau, bei row laeuft die Hauptachse horizontal."
|
|
},
|
|
{
|
|
"type": "order",
|
|
"prompt": "Zwei-Reihen-Layout bauen",
|
|
"points": 100,
|
|
"instruction": "Ordne die Schritte, um Items bei Bedarf in mehrere Reihen umbrechen zu lassen.",
|
|
"items": [
|
|
"display: flex;",
|
|
"flex-wrap: wrap;",
|
|
"gap: 16px;",
|
|
"justify-content: space-between;"
|
|
],
|
|
"hint": "Erst Flex aktivieren, dann Umbruch erlauben, dann verfeinern.",
|
|
"successText": "So entsteht ein sauberes, umbrechendes Layout."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Umbruch erlauben",
|
|
"points": 100,
|
|
"narrative": "Passen die Items nicht in eine Zeile, sollen sie umbrechen.",
|
|
"codeTemplate": ".grid {\n display: flex;\n {{1}}: wrap;\n}",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Property",
|
|
"hint": "Erlaubt mehrzeilige Flex-Container.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"flex-wrap"
|
|
]
|
|
}
|
|
],
|
|
"successText": "flex-wrap: wrap bricht volle Zeilen automatisch um."
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Gleiche Hoehe",
|
|
"points": 100,
|
|
"question": "Welcher align-items-Wert streckt alle Items auf die volle Hoehe des Containers?",
|
|
"options": [
|
|
"stretch",
|
|
"center",
|
|
"baseline",
|
|
"flex-start"
|
|
],
|
|
"correctIndex": 0,
|
|
"hint": "Es ist auch der Standardwert.",
|
|
"successText": "stretch dehnt die Items ueber die Querachse."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Querachse zentrieren",
|
|
"points": 100,
|
|
"narrative": "Die Icons in der Leiste sollen vertikal mittig sitzen.",
|
|
"codeTemplate": ".bar {\n display: flex;\n {{1}}: center;\n}",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Property",
|
|
"hint": "Wirkt auf der Querachse.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"align-items"
|
|
]
|
|
}
|
|
],
|
|
"successText": "align-items: center zentriert auf der Querachse."
|
|
},
|
|
{
|
|
"type": "match",
|
|
"prompt": "Verteilungs-Werte",
|
|
"points": 100,
|
|
"instruction": "Ordne jeden justify-content-Wert seiner Wirkung zu.",
|
|
"pairs": [
|
|
[
|
|
"center",
|
|
"Mittig zusammen"
|
|
],
|
|
[
|
|
"space-between",
|
|
"Aussen buendig, Rest verteilt"
|
|
],
|
|
[
|
|
"flex-end",
|
|
"Am Ende der Achse"
|
|
],
|
|
[
|
|
"space-evenly",
|
|
"Ueberall gleicher Abstand"
|
|
]
|
|
],
|
|
"hint": "Achte auf die Raender.",
|
|
"successText": "Du beherrschst die Verteilungs-Werte!"
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Verfuegbaren Platz fuellen",
|
|
"points": 100,
|
|
"question": "Womit laesst du ein einzelnes Flex-Item den verbleibenden freien Platz einnehmen?",
|
|
"options": [
|
|
"flex-grow: 1",
|
|
"flex-shrink: 0",
|
|
"order: 1",
|
|
"align-self: stretch"
|
|
],
|
|
"correctIndex": 0,
|
|
"hint": "Es geht ums Wachsen, nicht ums Schrumpfen.",
|
|
"successText": "flex-grow: 1 laesst das Item expandieren."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Abstand setzen",
|
|
"points": 100,
|
|
"narrative": "Zwischen den Werkzeug-Buttons soll ein fester Abstand liegen.",
|
|
"codeTemplate": ".toolbar {\n display: flex;\n {{1}}: 12px;\n}",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Property",
|
|
"hint": "Funktioniert auch in Flex-Containern.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"gap"
|
|
]
|
|
}
|
|
],
|
|
"successText": "gap haelt die Items gleichmaessig auf Abstand."
|
|
},
|
|
{
|
|
"type": "order",
|
|
"prompt": "Item perfekt zentrieren",
|
|
"points": 100,
|
|
"instruction": "Ordne die Schritte, um ein einzelnes Item horizontal UND vertikal zu zentrieren.",
|
|
"items": [
|
|
"display: flex;",
|
|
"justify-content: center;",
|
|
"align-items: center;"
|
|
],
|
|
"hint": "Erst Flex, dann beide Achsen zentrieren.",
|
|
"successText": "Der Klassiker fuer perfekte Zentrierung!"
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Kurzschreibweise flex",
|
|
"points": 100,
|
|
"question": "Was bedeutet die Kurzform flex: 1; ?",
|
|
"options": [
|
|
"flex-grow: 1; flex-shrink: 1; flex-basis: 0%",
|
|
"Nur flex-grow: 1",
|
|
"flex-basis: 1px",
|
|
"flex-wrap: 1"
|
|
],
|
|
"correctIndex": 0,
|
|
"hint": "Die Kurzform setzt drei Eigenschaften auf einmal.",
|
|
"successText": "Richtig, flex: 1 entspricht 1 1 0%."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Reihenfolge umkehren",
|
|
"points": 100,
|
|
"narrative": "Die Items sollen horizontal liegen, aber von rechts nach links angeordnet sein.",
|
|
"codeTemplate": ".row {\n display: flex;\n flex-direction: {{1}};\n}",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Wert",
|
|
"hint": "Reihe, aber umgekehrt.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"row-reverse"
|
|
]
|
|
}
|
|
],
|
|
"successText": "row-reverse dreht die Hauptachsen-Richtung um."
|
|
},
|
|
{
|
|
"type": "match",
|
|
"prompt": "Achsen unterscheiden",
|
|
"points": 100,
|
|
"instruction": "Ordne jede Property der Achse zu, auf der sie wirkt.",
|
|
"pairs": [
|
|
[
|
|
"justify-content",
|
|
"Hauptachse"
|
|
],
|
|
[
|
|
"align-items",
|
|
"Querachse"
|
|
],
|
|
[
|
|
"flex-direction",
|
|
"Definiert die Hauptachse"
|
|
],
|
|
[
|
|
"align-self",
|
|
"Einzelnes Item, Querachse"
|
|
]
|
|
],
|
|
"hint": "justify = Haupt, align = Quer.",
|
|
"successText": "Du trennst Haupt- und Querachse sicher!"
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Querachse benennen",
|
|
"points": 100,
|
|
"question": "Bei flex-direction: row verlaeuft die Querachse ...",
|
|
"options": [
|
|
"vertikal (oben nach unten)",
|
|
"horizontal (links nach rechts)",
|
|
"diagonal",
|
|
"im Kreis"
|
|
],
|
|
"correctIndex": 0,
|
|
"hint": "Die Querachse steht senkrecht zur Hauptachse.",
|
|
"successText": "Genau, bei row ist die Querachse vertikal."
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"slug": "regex-master",
|
|
"title": "Regex Master",
|
|
"language": "Regex",
|
|
"path": "syntax",
|
|
"description": "Schreibe reguläre Ausdrücke und lass sie alle Beispiele korrekt klassifizieren.",
|
|
"tasks": [
|
|
{
|
|
"type": "regex",
|
|
"prompt": "Nur Ziffern",
|
|
"points": 100,
|
|
"instruction": "Schreibe ein Muster, das nur aus einer oder mehreren Ziffern besteht.",
|
|
"samples": [
|
|
{
|
|
"text": "12345",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "007",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "12a",
|
|
"shouldMatch": false
|
|
},
|
|
{
|
|
"text": "",
|
|
"shouldMatch": false
|
|
},
|
|
{
|
|
"text": "3 4",
|
|
"shouldMatch": false
|
|
}
|
|
],
|
|
"pattern": "^\\d+$",
|
|
"hint": "\\d steht für eine Ziffer, + für ein oder mehr.",
|
|
"successText": "Saubere Ziffernfolge!"
|
|
},
|
|
{
|
|
"type": "regex",
|
|
"prompt": "Nur Kleinbuchstaben",
|
|
"points": 100,
|
|
"instruction": "Akzeptiere ausschließlich Kleinbuchstaben a-z (mindestens einen).",
|
|
"samples": [
|
|
{
|
|
"text": "hallo",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "abc",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "Hallo",
|
|
"shouldMatch": false
|
|
},
|
|
{
|
|
"text": "abc123",
|
|
"shouldMatch": false
|
|
},
|
|
{
|
|
"text": "",
|
|
"shouldMatch": false
|
|
}
|
|
],
|
|
"pattern": "^[a-z]+$",
|
|
"hint": "Eine Zeichenklasse [a-z] deckt genau die Kleinbuchstaben ab.",
|
|
"successText": "Alles klein geschrieben."
|
|
},
|
|
{
|
|
"type": "regex",
|
|
"prompt": "Binärzahl",
|
|
"points": 100,
|
|
"instruction": "Erlaube nur die Ziffern 0 und 1.",
|
|
"samples": [
|
|
{
|
|
"text": "1010",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "0",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "102",
|
|
"shouldMatch": false
|
|
},
|
|
{
|
|
"text": "",
|
|
"shouldMatch": false
|
|
},
|
|
{
|
|
"text": "10 01",
|
|
"shouldMatch": false
|
|
}
|
|
],
|
|
"pattern": "^[01]+$",
|
|
"hint": "Nur die beiden Zeichen 0 und 1 sind erlaubt.",
|
|
"successText": "0 und 1, mehr braucht es nicht."
|
|
},
|
|
{
|
|
"type": "regex",
|
|
"prompt": "Postleitzahl",
|
|
"points": 100,
|
|
"instruction": "Eine deutsche PLZ besteht aus genau 5 Ziffern.",
|
|
"samples": [
|
|
{
|
|
"text": "12345",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "01067",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "1234",
|
|
"shouldMatch": false
|
|
},
|
|
{
|
|
"text": "123456",
|
|
"shouldMatch": false
|
|
},
|
|
{
|
|
"text": "12a45",
|
|
"shouldMatch": false
|
|
}
|
|
],
|
|
"pattern": "^\\d{5}$",
|
|
"hint": "Mit {5} legst du eine feste Anzahl fest.",
|
|
"successText": "Genau 5 Stellen!"
|
|
},
|
|
{
|
|
"type": "regex",
|
|
"prompt": "CSS-Datei",
|
|
"points": 100,
|
|
"instruction": "Dateiname aus Wortzeichen/Bindestrich, der auf .css endet.",
|
|
"samples": [
|
|
{
|
|
"text": "style.css",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "main.css",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "style.scss",
|
|
"shouldMatch": false
|
|
},
|
|
{
|
|
"text": "style.css.map",
|
|
"shouldMatch": false
|
|
},
|
|
{
|
|
"text": "styles",
|
|
"shouldMatch": false
|
|
}
|
|
],
|
|
"pattern": "^[\\w-]+\\.css$",
|
|
"hint": "Den Punkt musst du als \\. maskieren.",
|
|
"successText": "Endung .css erkannt."
|
|
},
|
|
{
|
|
"type": "regex",
|
|
"prompt": "Hex-Farbe",
|
|
"points": 100,
|
|
"instruction": "Eine Hex-Farbe: # gefolgt von genau 6 Hex-Ziffern.",
|
|
"samples": [
|
|
{
|
|
"text": "#ff0000",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "#A1B2C3",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "#fff",
|
|
"shouldMatch": false
|
|
},
|
|
{
|
|
"text": "ff0000",
|
|
"shouldMatch": false
|
|
},
|
|
{
|
|
"text": "#gggggg",
|
|
"shouldMatch": false
|
|
}
|
|
],
|
|
"pattern": "^#[0-9a-fA-F]{6}$",
|
|
"hint": "Hex-Ziffern sind 0-9 sowie a-f und A-F.",
|
|
"successText": "Perfekte Farbe!"
|
|
},
|
|
{
|
|
"type": "regex",
|
|
"prompt": "Eigenname",
|
|
"points": 100,
|
|
"instruction": "Erster Buchstabe groß, danach nur Kleinbuchstaben.",
|
|
"samples": [
|
|
{
|
|
"text": "Max",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "Anna",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "max",
|
|
"shouldMatch": false
|
|
},
|
|
{
|
|
"text": "MAX",
|
|
"shouldMatch": false
|
|
},
|
|
{
|
|
"text": "Max1",
|
|
"shouldMatch": false
|
|
}
|
|
],
|
|
"pattern": "^[A-Z][a-z]+$",
|
|
"hint": "Ein Großbuchstabe, dann eine Folge Kleinbuchstaben.",
|
|
"successText": "Groß angefangen, klein weiter."
|
|
},
|
|
{
|
|
"type": "regex",
|
|
"prompt": "Variablenname",
|
|
"points": 100,
|
|
"instruction": "Beginnt mit Buchstabe oder _, danach Wortzeichen.",
|
|
"samples": [
|
|
{
|
|
"text": "userName",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "_count",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "user_1",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "1user",
|
|
"shouldMatch": false
|
|
},
|
|
{
|
|
"text": "user name",
|
|
"shouldMatch": false
|
|
},
|
|
{
|
|
"text": "user-name",
|
|
"shouldMatch": false
|
|
}
|
|
],
|
|
"pattern": "^[a-zA-Z_]\\w*$",
|
|
"hint": "\\w steht für Buchstaben, Ziffern und Unterstrich.",
|
|
"successText": "Gültiger Bezeichner!"
|
|
},
|
|
{
|
|
"type": "regex",
|
|
"prompt": "Benutzername",
|
|
"points": 100,
|
|
"instruction": "3 bis 16 Wortzeichen (Buchstaben, Ziffern, Unterstrich).",
|
|
"samples": [
|
|
{
|
|
"text": "max_99",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "abc",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "ab",
|
|
"shouldMatch": false
|
|
},
|
|
{
|
|
"text": "this_is_way_too_long_name",
|
|
"shouldMatch": false
|
|
},
|
|
{
|
|
"text": "max mustermann",
|
|
"shouldMatch": false
|
|
}
|
|
],
|
|
"pattern": "^\\w{3,16}$",
|
|
"hint": "Mit {3,16} gibst du eine Spanne an.",
|
|
"successText": "Länge passt!"
|
|
},
|
|
{
|
|
"type": "regex",
|
|
"prompt": "URL-Slug",
|
|
"points": 100,
|
|
"instruction": "Kleinbuchstaben/Ziffern, mit einzelnen Bindestrichen getrennt.",
|
|
"samples": [
|
|
{
|
|
"text": "mein-erster-post",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "blog",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "abc123",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "Mein-Post",
|
|
"shouldMatch": false
|
|
},
|
|
{
|
|
"text": "mein--post",
|
|
"shouldMatch": false
|
|
},
|
|
{
|
|
"text": "mein-post-",
|
|
"shouldMatch": false
|
|
}
|
|
],
|
|
"pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
|
|
"hint": "Kein Bindestrich am Anfang/Ende und keine doppelten.",
|
|
"successText": "Sauberer Slug!"
|
|
},
|
|
{
|
|
"type": "regex",
|
|
"prompt": "E-Mail (.de)",
|
|
"points": 100,
|
|
"instruction": "Einfache E-Mail mit @ und Endung .de.",
|
|
"samples": [
|
|
{
|
|
"text": "max@web.de",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "info@firma.de",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "max@web.com",
|
|
"shouldMatch": false
|
|
},
|
|
{
|
|
"text": "maxweb.de",
|
|
"shouldMatch": false
|
|
},
|
|
{
|
|
"text": "MAX@WEB.DE",
|
|
"shouldMatch": false
|
|
}
|
|
],
|
|
"pattern": "^[\\w.-]+@[\\w.-]+\\.de$",
|
|
"hint": "Verankere die Endung mit \\.de am Stringende.",
|
|
"successText": "Gültige .de-Adresse!"
|
|
},
|
|
{
|
|
"type": "regex",
|
|
"prompt": "Dezimalzahl",
|
|
"points": 100,
|
|
"instruction": "Ganzzahl mit optionalem Vorzeichen und optionalem Nachkommateil (Punkt).",
|
|
"samples": [
|
|
{
|
|
"text": "3.14",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "42",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "-0.5",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "3.",
|
|
"shouldMatch": false
|
|
},
|
|
{
|
|
"text": ".5",
|
|
"shouldMatch": false
|
|
},
|
|
{
|
|
"text": "1,5",
|
|
"shouldMatch": false
|
|
}
|
|
],
|
|
"pattern": "^-?\\d+(\\.\\d+)?$",
|
|
"hint": "Der Nachkommateil ist eine optionale Gruppe (...)?.",
|
|
"successText": "Zahl korrekt erkannt."
|
|
},
|
|
{
|
|
"type": "regex",
|
|
"prompt": "Uhrzeit HH:MM",
|
|
"points": 100,
|
|
"instruction": "24-Stunden-Uhrzeit von 00:00 bis 23:59.",
|
|
"samples": [
|
|
{
|
|
"text": "09:30",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "23:59",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "00:00",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "24:00",
|
|
"shouldMatch": false
|
|
},
|
|
{
|
|
"text": "9:30",
|
|
"shouldMatch": false
|
|
},
|
|
{
|
|
"text": "12:60",
|
|
"shouldMatch": false
|
|
}
|
|
],
|
|
"pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]$",
|
|
"hint": "Trenne die Stundenbereiche mit einer Alternative |.",
|
|
"successText": "Zeit im gültigen Bereich!"
|
|
},
|
|
{
|
|
"type": "regex",
|
|
"prompt": "Telefonnummer",
|
|
"points": 100,
|
|
"instruction": "Beginnt mit +49 oder 0, danach 3 bis 12 Ziffern.",
|
|
"samples": [
|
|
{
|
|
"text": "+4915112345",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "017212345",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "12345678",
|
|
"shouldMatch": false
|
|
},
|
|
{
|
|
"text": "+49",
|
|
"shouldMatch": false
|
|
},
|
|
{
|
|
"text": "+1234567",
|
|
"shouldMatch": false
|
|
},
|
|
{
|
|
"text": "0",
|
|
"shouldMatch": false
|
|
}
|
|
],
|
|
"pattern": "^(\\+49|0)[0-9]{3,12}$",
|
|
"hint": "Das + ist ein Sonderzeichen und muss als \\+ maskiert werden.",
|
|
"successText": "Nummer im richtigen Format!"
|
|
},
|
|
{
|
|
"type": "regex",
|
|
"prompt": "IPv4-Adresse",
|
|
"points": 100,
|
|
"instruction": "Vier durch Punkte getrennte Zahlengruppen (grob, 1-3 Ziffern).",
|
|
"samples": [
|
|
{
|
|
"text": "192.168.0.1",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "8.8.8.8",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "255.255.255.255",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "192.168.0",
|
|
"shouldMatch": false
|
|
},
|
|
{
|
|
"text": "192.168.0.1.5",
|
|
"shouldMatch": false
|
|
},
|
|
{
|
|
"text": "1234.0.0.1",
|
|
"shouldMatch": false
|
|
}
|
|
],
|
|
"pattern": "^\\d{1,3}(\\.\\d{1,3}){3}$",
|
|
"hint": "Eine Gruppe (\\.\\d{1,3}) wiederholt sich genau dreimal.",
|
|
"successText": "Vier Oktette erkannt!"
|
|
},
|
|
{
|
|
"type": "regex",
|
|
"prompt": "Datum ISO",
|
|
"points": 100,
|
|
"instruction": "Datum im Format YYYY-MM-DD mit gültigem Monat/Tag-Bereich.",
|
|
"samples": [
|
|
{
|
|
"text": "2026-06-30",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "1999-12-31",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "2026-13-01",
|
|
"shouldMatch": false
|
|
},
|
|
{
|
|
"text": "2026-00-10",
|
|
"shouldMatch": false
|
|
},
|
|
{
|
|
"text": "26-06-30",
|
|
"shouldMatch": false
|
|
},
|
|
{
|
|
"text": "2026/06/30",
|
|
"shouldMatch": false
|
|
}
|
|
],
|
|
"pattern": "^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$",
|
|
"hint": "Begrenze Monat auf 01-12 und Tag auf 01-31 mit Alternativen.",
|
|
"successText": "Gültiges ISO-Datum!"
|
|
},
|
|
{
|
|
"type": "regex",
|
|
"prompt": "Datum deutsch",
|
|
"points": 100,
|
|
"instruction": "Datum im Format DD.MM.YYYY mit gültigem Tag/Monat-Bereich.",
|
|
"samples": [
|
|
{
|
|
"text": "30.06.2026",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "01.01.2000",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "31.13.2026",
|
|
"shouldMatch": false
|
|
},
|
|
{
|
|
"text": "1.6.2026",
|
|
"shouldMatch": false
|
|
},
|
|
{
|
|
"text": "30/06/2026",
|
|
"shouldMatch": false
|
|
},
|
|
{
|
|
"text": "32.06.2026",
|
|
"shouldMatch": false
|
|
}
|
|
],
|
|
"pattern": "^(0[1-9]|[12][0-9]|3[01])\\.(0[1-9]|1[0-2])\\.\\d{4}$",
|
|
"hint": "Die Trennpunkte musst du als \\. schreiben.",
|
|
"successText": "Datum sitzt!"
|
|
},
|
|
{
|
|
"type": "regex",
|
|
"prompt": "Webadresse",
|
|
"points": 100,
|
|
"instruction": "URL mit http oder https, Domain und optionalem Pfad.",
|
|
"samples": [
|
|
{
|
|
"text": "http://example.com",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "https://www.google.de/search",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "https://test.io/path/to/page",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "ftp://example.com",
|
|
"shouldMatch": false
|
|
},
|
|
{
|
|
"text": "http//example.com",
|
|
"shouldMatch": false
|
|
},
|
|
{
|
|
"text": "example.com",
|
|
"shouldMatch": false
|
|
}
|
|
],
|
|
"pattern": "^https?://[\\w.-]+\\.[a-z]{2,}(/.*)?$",
|
|
"hint": "Das s in https ist optional: https? .",
|
|
"successText": "Gültige URL!"
|
|
},
|
|
{
|
|
"type": "regex",
|
|
"prompt": "Kreditkartennummer",
|
|
"points": 100,
|
|
"instruction": "16 Ziffern in vier Vierergruppen, durch Leerzeichen getrennt.",
|
|
"samples": [
|
|
{
|
|
"text": "1234 5678 9012 3456",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "0000 0000 0000 0000",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "1234567890123456",
|
|
"shouldMatch": false
|
|
},
|
|
{
|
|
"text": "1234 5678 9012",
|
|
"shouldMatch": false
|
|
},
|
|
{
|
|
"text": "12 34 56 78",
|
|
"shouldMatch": false
|
|
}
|
|
],
|
|
"pattern": "^(\\d{4} ){3}\\d{4}$",
|
|
"hint": "Drei Gruppen mit Leerzeichen, dann eine letzte ohne.",
|
|
"successText": "Format stimmt!"
|
|
},
|
|
{
|
|
"type": "regex",
|
|
"prompt": "Wortgrenze: PHP",
|
|
"points": 100,
|
|
"instruction": "Der Text enthält das eigenständige Wort PHP (als ganzes Wort).",
|
|
"samples": [
|
|
{
|
|
"text": "Ich liebe PHP sehr",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "PHP ist toll",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "PHPStorm ist gut",
|
|
"shouldMatch": false
|
|
},
|
|
{
|
|
"text": "Ich mag Python",
|
|
"shouldMatch": false
|
|
},
|
|
{
|
|
"text": "phpmyadmin laden",
|
|
"shouldMatch": false
|
|
}
|
|
],
|
|
"pattern": "^.*\\bPHP\\b.*$",
|
|
"hint": "\\b markiert eine Wortgrenze rund um das Wort.",
|
|
"successText": "Ganzes Wort erkannt!"
|
|
},
|
|
{
|
|
"type": "regex",
|
|
"prompt": "Sicheres Passwort",
|
|
"points": 100,
|
|
"instruction": "Mindestens 8 Zeichen und enthält mindestens einen Buchstaben und eine Ziffer.",
|
|
"samples": [
|
|
{
|
|
"text": "Passwort1",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "abc12345",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "Test1234",
|
|
"shouldMatch": true
|
|
},
|
|
{
|
|
"text": "password",
|
|
"shouldMatch": false
|
|
},
|
|
{
|
|
"text": "12345678",
|
|
"shouldMatch": false
|
|
},
|
|
{
|
|
"text": "abc123",
|
|
"shouldMatch": false
|
|
}
|
|
],
|
|
"pattern": "^(?=.*[A-Za-z])(?=.*\\d).{8,}$",
|
|
"hint": "Mit Lookaheads (?=...) erzwingst du beide Bedingungen.",
|
|
"successText": "Stark genug!"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"slug": "php-request",
|
|
"title": "PHP Request",
|
|
"language": "PHP",
|
|
"path": "debugging",
|
|
"description": "Verarbeite Formulare, Sessions und Datenbanken sicher in PHP.",
|
|
"tasks": [
|
|
{
|
|
"type": "choice",
|
|
"prompt": "POST-Formular lesen",
|
|
"points": 100,
|
|
"question": "Ein Formular wird mit method=\"post\" abgeschickt. Über welche Superglobale liest du die Daten?",
|
|
"options": [
|
|
"$_GET",
|
|
"$_POST",
|
|
"$_SESSION",
|
|
"$_COOKIE"
|
|
],
|
|
"correctIndex": 1,
|
|
"hint": "Die Superglobale heißt wie die HTTP-Methode.",
|
|
"successText": "Richtig, $_POST enthält die Formulardaten."
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "URL-Parameter lesen",
|
|
"points": 100,
|
|
"question": "Werte aus der URL wie ?id=5 liest du mit welcher Superglobale?",
|
|
"options": [
|
|
"$_POST",
|
|
"$_GET",
|
|
"$_FILES",
|
|
"$_ENV"
|
|
],
|
|
"correctIndex": 1,
|
|
"hint": "Query-Parameter gehören zur GET-Methode.",
|
|
"successText": "Korrekt, $_GET liest die URL-Parameter."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "POST-Wert auslesen",
|
|
"points": 100,
|
|
"narrative": "Lies das Feld email aus den gesendeten Formulardaten.",
|
|
"codeTemplate": "$email = {{1}}[\"email\"];",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Superglobale",
|
|
"hint": "Array mit den POST-Daten.",
|
|
"caseSensitive": true,
|
|
"solutions": [
|
|
"$_POST"
|
|
]
|
|
}
|
|
],
|
|
"successText": "Stimmt, $_POST liefert die Formularwerte."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Session starten",
|
|
"points": 100,
|
|
"narrative": "Bevor du auf $_SESSION zugreifst, musst du die Session starten.",
|
|
"codeTemplate": "<?php\n{{1}}();\n$_SESSION[\"user\"] = \"Max\";",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Funktion",
|
|
"hint": "Startet oder setzt die Session fort.",
|
|
"caseSensitive": true,
|
|
"solutions": [
|
|
"session_start"
|
|
]
|
|
}
|
|
],
|
|
"successText": "Genau, session_start() muss zuerst laufen."
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Existenz prüfen",
|
|
"points": 100,
|
|
"question": "Welche Funktion prüft, ob ein Array-Schlüssel existiert und nicht null ist?",
|
|
"options": [
|
|
"empty",
|
|
"isset",
|
|
"is_null",
|
|
"array_key"
|
|
],
|
|
"correctIndex": 1,
|
|
"hint": "Sie gibt false zurück, wenn die Variable nicht gesetzt ist.",
|
|
"successText": "Richtig, isset() ist die passende Wahl."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Wert absichern",
|
|
"points": 100,
|
|
"narrative": "Greife nur auf das POST-Feld zu, wenn es wirklich existiert.",
|
|
"codeTemplate": "if ({{1}}($_POST[\"email\"])) {\n $email = $_POST[\"email\"];\n}",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Prüf-Funktion",
|
|
"hint": "Prüft, ob die Variable existiert und nicht null ist.",
|
|
"caseSensitive": true,
|
|
"solutions": [
|
|
"isset"
|
|
]
|
|
}
|
|
],
|
|
"successText": "Sauber abgesichert mit isset()."
|
|
},
|
|
{
|
|
"type": "spot",
|
|
"prompt": "Falsche Superglobale",
|
|
"points": 100,
|
|
"instruction": "Das Formular nutzt method=\"post\". Klick den fehlerhaften Token an.",
|
|
"tokens": [
|
|
"$email",
|
|
"=",
|
|
"$_GET",
|
|
"[",
|
|
"'email'",
|
|
"]",
|
|
";"
|
|
],
|
|
"bugIndex": 2,
|
|
"fix": "$_POST",
|
|
"hint": "POST-Daten liegen nicht in $_GET.",
|
|
"successText": "Korrekt, hier muss $_POST stehen."
|
|
},
|
|
{
|
|
"type": "spot",
|
|
"prompt": "Zuweisung statt Vergleich",
|
|
"points": 100,
|
|
"instruction": "Hier soll verglichen statt zugewiesen werden. Klick den fehlerhaften Token an.",
|
|
"tokens": [
|
|
"if",
|
|
"(",
|
|
"$alter",
|
|
"=",
|
|
"18",
|
|
")"
|
|
],
|
|
"bugIndex": 3,
|
|
"fix": "==",
|
|
"hint": "Ein einzelnes = weist zu, statt zu vergleichen.",
|
|
"successText": "Richtig, == vergleicht die Werte."
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Login speichern",
|
|
"points": 100,
|
|
"question": "Wo speicherst du, dass ein Nutzer eingeloggt ist, sodass es über mehrere Requests erhalten bleibt?",
|
|
"options": [
|
|
"$_GET",
|
|
"$_POST",
|
|
"$_SESSION",
|
|
"$_SERVER"
|
|
],
|
|
"correctIndex": 2,
|
|
"hint": "Diese Daten überdauern einzelne Anfragen.",
|
|
"successText": "Genau, der Login-Status gehört in $_SESSION."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Cookie setzen",
|
|
"points": 100,
|
|
"narrative": "Setze ein Cookie, das eine Stunde lang gültig ist.",
|
|
"codeTemplate": "{{1}}(\"theme\", \"dark\", time() + 3600);",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Funktion",
|
|
"hint": "Sendet ein Cookie an den Browser.",
|
|
"caseSensitive": true,
|
|
"solutions": [
|
|
"setcookie"
|
|
]
|
|
}
|
|
],
|
|
"successText": "Cookie gesetzt mit setcookie()."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Statement vorbereiten",
|
|
"points": 100,
|
|
"narrative": "Nutze ein Prepared Statement, um SQL-Injection zu verhindern.",
|
|
"codeTemplate": "$stmt = $pdo->{{1}}(\"SELECT * FROM users WHERE email = ?\");",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "PDO-Methode",
|
|
"hint": "Bereitet die SQL-Anweisung vor.",
|
|
"caseSensitive": true,
|
|
"solutions": [
|
|
"prepare"
|
|
]
|
|
}
|
|
],
|
|
"successText": "Richtig, prepare() erstellt das Statement."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Statement ausführen",
|
|
"points": 100,
|
|
"narrative": "Führe das vorbereitete Statement mit dem gebundenen Wert aus.",
|
|
"codeTemplate": "$stmt->{{1}}([$email]);",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "PDO-Methode",
|
|
"hint": "Führt das Prepared Statement mit den Parametern aus.",
|
|
"caseSensitive": true,
|
|
"solutions": [
|
|
"execute"
|
|
]
|
|
}
|
|
],
|
|
"successText": "Ausgeführt mit execute()."
|
|
},
|
|
{
|
|
"type": "spot",
|
|
"prompt": "SQL-Injection",
|
|
"points": 100,
|
|
"instruction": "SQL-Injection-Gefahr! Klick den Token an, der so nicht direkt in die Query gehört.",
|
|
"tokens": [
|
|
"$sql",
|
|
"=",
|
|
"\"... WHERE id = \"",
|
|
".",
|
|
"$id"
|
|
],
|
|
"bugIndex": 4,
|
|
"fix": "?",
|
|
"hint": "Nutze einen Platzhalter und binde den Wert per Prepared Statement.",
|
|
"successText": "Richtig, statt $id gehört ein Platzhalter ? in die Query."
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Passwort prüfen",
|
|
"points": 100,
|
|
"question": "Mit welcher Funktion prüfst du ein eingegebenes Passwort gegen einen gespeicherten Hash?",
|
|
"options": [
|
|
"password_hash",
|
|
"password_verify",
|
|
"md5",
|
|
"strcmp"
|
|
],
|
|
"correctIndex": 1,
|
|
"hint": "Sie vergleicht Klartext mit einem Hash.",
|
|
"successText": "Korrekt, password_verify() ist sicher."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Passwort hashen",
|
|
"points": 100,
|
|
"narrative": "Speichere niemals Klartext-Passwörter. Erzeuge einen sicheren Hash.",
|
|
"codeTemplate": "$hash = {{1}}($passwort, PASSWORD_DEFAULT);",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Funktion",
|
|
"hint": "Erzeugt einen sicheren Passwort-Hash.",
|
|
"caseSensitive": true,
|
|
"solutions": [
|
|
"password_hash"
|
|
]
|
|
}
|
|
],
|
|
"successText": "Sicher gehasht mit password_hash()."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Weiterleitung",
|
|
"points": 100,
|
|
"narrative": "Leite den Browser nach dem Login auf das Dashboard um.",
|
|
"codeTemplate": "{{1}}(\"Location: /dashboard.php\");\nexit;",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Funktion",
|
|
"hint": "Sendet einen rohen HTTP-Header.",
|
|
"caseSensitive": true,
|
|
"solutions": [
|
|
"header"
|
|
]
|
|
}
|
|
],
|
|
"successText": "Weiterleitung per header() gesetzt."
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"slug": "api-explorer",
|
|
"title": "API Explorer",
|
|
"language": "REST",
|
|
"path": "visual",
|
|
"description": "Lerne HTTP-Methoden, Statuscodes und REST-Routen kennen.",
|
|
"tasks": [
|
|
{
|
|
"type": "match",
|
|
"prompt": "Statuscodes zuordnen",
|
|
"points": 100,
|
|
"instruction": "Ordne die HTTP-Statuscodes ihrer Bedeutung zu.",
|
|
"pairs": [
|
|
[
|
|
"200",
|
|
"OK"
|
|
],
|
|
[
|
|
"201",
|
|
"Created"
|
|
],
|
|
[
|
|
"404",
|
|
"Not Found"
|
|
],
|
|
[
|
|
"500",
|
|
"Internal Server Error"
|
|
]
|
|
],
|
|
"hint": "2xx ist Erfolg, 4xx Client-Fehler, 5xx Server-Fehler.",
|
|
"successText": "Alle Codes richtig zugeordnet!"
|
|
},
|
|
{
|
|
"type": "match",
|
|
"prompt": "Methoden und Zweck",
|
|
"points": 100,
|
|
"instruction": "Ordne jede HTTP-Methode ihrem Zweck zu.",
|
|
"pairs": [
|
|
[
|
|
"GET",
|
|
"Daten lesen"
|
|
],
|
|
[
|
|
"POST",
|
|
"Neu anlegen"
|
|
],
|
|
[
|
|
"PUT",
|
|
"Ersetzen"
|
|
],
|
|
[
|
|
"DELETE",
|
|
"Löschen"
|
|
]
|
|
],
|
|
"hint": "Denk an CRUD: lesen, anlegen, ändern, löschen.",
|
|
"successText": "Methoden sitzen!"
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Ressource anlegen",
|
|
"points": 100,
|
|
"question": "Welche HTTP-Methode legt eine neue Ressource an?",
|
|
"options": [
|
|
"GET",
|
|
"POST",
|
|
"PUT",
|
|
"DELETE"
|
|
],
|
|
"correctIndex": 1,
|
|
"hint": "Sie sendet Daten im Body zum Server.",
|
|
"successText": "Richtig, POST erstellt neue Ressourcen."
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Daten abrufen",
|
|
"points": 100,
|
|
"question": "Welche Methode nutzt du, um Daten abzurufen, ohne etwas zu ändern?",
|
|
"options": [
|
|
"GET",
|
|
"POST",
|
|
"PATCH",
|
|
"DELETE"
|
|
],
|
|
"correctIndex": 0,
|
|
"hint": "Diese Methode ist sicher und verändert nichts.",
|
|
"successText": "Korrekt, GET liest nur."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Nutzer abrufen",
|
|
"points": 100,
|
|
"narrative": "Du willst den Nutzer mit der ID 7 abrufen.",
|
|
"codeTemplate": "{{1}} /api/users/7",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "HTTP-Methode",
|
|
"hint": "Daten lesen, ohne sie zu ändern.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"GET"
|
|
]
|
|
}
|
|
],
|
|
"successText": "Genau, GET liest die Ressource."
|
|
},
|
|
{
|
|
"type": "match",
|
|
"prompt": "Code-Klassen",
|
|
"points": 100,
|
|
"instruction": "Ordne die Statuscode-Klassen ihrer Bedeutung zu.",
|
|
"pairs": [
|
|
[
|
|
"2xx",
|
|
"Erfolg"
|
|
],
|
|
[
|
|
"3xx",
|
|
"Weiterleitung"
|
|
],
|
|
[
|
|
"4xx",
|
|
"Client-Fehler"
|
|
],
|
|
[
|
|
"5xx",
|
|
"Server-Fehler"
|
|
]
|
|
],
|
|
"hint": "Die erste Ziffer verrät die Kategorie.",
|
|
"successText": "Klassen verstanden!"
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Erfolgreich erstellt",
|
|
"points": 100,
|
|
"question": "Welcher Statuscode signalisiert, dass eine Ressource erfolgreich erstellt wurde?",
|
|
"options": [
|
|
"200",
|
|
"201",
|
|
"204",
|
|
"404"
|
|
],
|
|
"correctIndex": 1,
|
|
"hint": "Created hat einen eigenen 2xx-Code.",
|
|
"successText": "Richtig, 201 Created."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Nutzer anlegen",
|
|
"points": 100,
|
|
"narrative": "Du legst einen neuen Nutzer an.",
|
|
"codeTemplate": "{{1}} /api/users",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "HTTP-Methode",
|
|
"hint": "Neu anlegen mit Daten im Body.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"POST"
|
|
]
|
|
}
|
|
],
|
|
"successText": "Korrekt, POST legt an."
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Nicht gefunden",
|
|
"points": 100,
|
|
"question": "Eine angeforderte Ressource existiert nicht. Welcher Statuscode passt?",
|
|
"options": [
|
|
"200",
|
|
"301",
|
|
"404",
|
|
"500"
|
|
],
|
|
"correctIndex": 2,
|
|
"hint": "Der berühmteste Fehlercode im Web.",
|
|
"successText": "Richtig, 404 Not Found."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Statuszeile Erfolg",
|
|
"points": 100,
|
|
"narrative": "Eine erfolgreiche Antwort beginnt mit dieser Statuszeile.",
|
|
"codeTemplate": "HTTP/1.1 {{1}} OK",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Statuscode",
|
|
"hint": "Der Standard-Code für Erfolg.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"200"
|
|
]
|
|
}
|
|
],
|
|
"successText": "Genau, 200 OK."
|
|
},
|
|
{
|
|
"type": "match",
|
|
"prompt": "Weitere Codes",
|
|
"points": 100,
|
|
"instruction": "Ordne die Statuscodes ihrer Bedeutung zu.",
|
|
"pairs": [
|
|
[
|
|
"301",
|
|
"Moved Permanently"
|
|
],
|
|
[
|
|
"400",
|
|
"Bad Request"
|
|
],
|
|
[
|
|
"401",
|
|
"Unauthorized"
|
|
],
|
|
[
|
|
"403",
|
|
"Forbidden"
|
|
]
|
|
],
|
|
"hint": "401 = nicht eingeloggt, 403 = kein Zugriff trotz Login.",
|
|
"successText": "Stark, alles richtig!"
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Nicht authentifiziert",
|
|
"points": 100,
|
|
"question": "Der Client ist nicht authentifiziert. Welcher Statuscode ist korrekt?",
|
|
"options": [
|
|
"400",
|
|
"401",
|
|
"403",
|
|
"404"
|
|
],
|
|
"correctIndex": 1,
|
|
"hint": "Unauthorized meint hier: nicht eingeloggt.",
|
|
"successText": "Richtig, 401 Unauthorized."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Statuszeile Fehler",
|
|
"points": 100,
|
|
"narrative": "Die Ressource fehlt. Vervollständige die Statuszeile.",
|
|
"codeTemplate": "HTTP/1.1 {{1}} Not Found",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Statuscode",
|
|
"hint": "Client fragt etwas an, das es nicht gibt.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"404"
|
|
]
|
|
}
|
|
],
|
|
"successText": "Korrekt, 404 Not Found."
|
|
},
|
|
{
|
|
"type": "choice",
|
|
"prompt": "Teil-Update",
|
|
"points": 100,
|
|
"question": "Welche Methode aktualisiert nur einzelne Felder einer Ressource?",
|
|
"options": [
|
|
"POST",
|
|
"PUT",
|
|
"PATCH",
|
|
"GET"
|
|
],
|
|
"correctIndex": 2,
|
|
"hint": "PUT ersetzt komplett, diese Methode nur teilweise.",
|
|
"successText": "Richtig, PATCH ändert einzelne Felder."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Nutzer löschen",
|
|
"points": 100,
|
|
"narrative": "Du löschst den Nutzer mit der ID 7.",
|
|
"codeTemplate": "{{1}} /api/users/7",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "HTTP-Methode",
|
|
"hint": "Entfernt die Ressource.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"DELETE"
|
|
]
|
|
}
|
|
],
|
|
"successText": "Genau, DELETE entfernt sie."
|
|
},
|
|
{
|
|
"type": "cloze",
|
|
"prompt": "Antwortformat",
|
|
"points": 100,
|
|
"narrative": "Die API antwortet mit JSON. Vervollständige den Header.",
|
|
"codeTemplate": "Content-Type: application/{{1}}",
|
|
"gaps": [
|
|
{
|
|
"position": 1,
|
|
"placeholder": "Format",
|
|
"hint": "Das gängige Datenformat moderner APIs.",
|
|
"caseSensitive": false,
|
|
"solutions": [
|
|
"json"
|
|
]
|
|
}
|
|
],
|
|
"successText": "Korrekt, application/json."
|
|
}
|
|
]
|
|
}
|
|
]
|
|
} |