diff --git a/.gamification.json b/.gamification.json
index 921a55a..023b899 100644
--- a/.gamification.json
+++ b/.gamification.json
@@ -13,5 +13,5 @@
"streak_tage": 1,
"letzter_aktiver_tag": "2026-08-29",
"historie": [],
- "zuletzt_aktualisiert": "2026-08-30T00:34:34.221863"
+ "zuletzt_aktualisiert": "2026-08-30T00:42:10.922005"
}
\ No newline at end of file
diff --git a/assets/certificate.js b/assets/certificate.js
index 1d82076..d6544cf 100644
--- a/assets/certificate.js
+++ b/assets/certificate.js
@@ -37,9 +37,25 @@
titel = "Python Professional & Data Engineering";
moduleDesc = "6 Master-Module: Reguläre Ausdrücke (re), Functional Programming (lambda, zip, map), Generatoren & itertools, Dataclasses & Type Hints, Relationale DBs mit SQLite, REST-APIs & JSON-Feeds";
trackKey = "lehrpfad_4";
+ } else if (lehrpfadNummer === "js_grundlagen") {
+ titel = "JavaScript ES6+ Grundlagen & DOM-Manipulation";
+ moduleDesc = "4 Module: Variablen (let/const), Arrow Functions, Arrays (map/filter/reduce), Objekte, Destructuring und DOM-Event-Driven Architecture";
+ trackKey = "js_grundlagen";
+ } else if (lehrpfadNummer === "js_async") {
+ titel = "Asynchrones JavaScript & REST-APIs";
+ moduleDesc = "2 Module: Callbacks, Event Loop, Promises, Chaining, Promise.all, Async/Await und REST-APIs mit Fetch & JSON";
+ trackKey = "js_async";
+ } else if (lehrpfadNummer === "js_master") {
+ titel = "Master of Modern JavaScript & TypeScript Development";
+ moduleDesc = "Vollständiger Abschluss des JS/TS Curriculums (8 Module, TypeScript Type Safety, Interfaces, Generics & Task-App Masterprojekt)";
+ trackKey = "js_master";
+ } else if (lehrpfadNummer === "sql_grundlagen") {
+ titel = "SQL & Relationale Datenbanken";
+ moduleDesc = "CREATE TABLE, Primary Keys, INSERT, SELECT, Filter mit WHERE und SQLite WebAssembly Abfragen";
+ trackKey = "sql_grundlagen";
} else if (lehrpfadNummer === "master") {
- titel = "Grandmaster of Python Software Engineering";
- moduleDesc = "Vollständiger Abschluss aller 4 Lehrpfade (33 Module, über 220 bestandene automatisierte Unittests, SQLite-Persistenz, REST-APIs & MVC-Desktop-App)";
+ titel = "Grandmaster of Software Engineering (Polyglot)";
+ moduleDesc = "Vollständiger Abschluss der plattformweiten Curricula (Python, JavaScript, TypeScript & SQL)";
trackKey = "master";
}
diff --git a/assets/gamification_data.js b/assets/gamification_data.js
index ebef1f2..aaec614 100644
--- a/assets/gamification_data.js
+++ b/assets/gamification_data.js
@@ -14,7 +14,7 @@ window.GAMIFICATION_DATA = {
"streak_tage": 1,
"letzter_aktiver_tag": "2026-08-29",
"historie": [],
- "zuletzt_aktualisiert": "2026-08-30T00:34:34.221863"
+ "zuletzt_aktualisiert": "2026-08-30T00:42:10.922005"
};
window.GAMIFICATION_BADGES_KATALOG = {
"rechenkonegin": {
diff --git a/assets/runners/js_runner.js b/assets/runners/js_runner.js
index 407a3c1..2bfce30 100644
--- a/assets/runners/js_runner.js
+++ b/assets/runners/js_runner.js
@@ -72,8 +72,19 @@
try {
const runFn = new Function(e.data.code);
- runFn();
- self.postMessage({ type: 'done', success: true });
+ const result = runFn();
+ if (result && typeof result.then === 'function') {
+ result
+ .then(() => {
+ self.postMessage({ type: 'done', success: true });
+ })
+ .catch((err) => {
+ self.postMessage({ type: 'stderr', text: '\\n❌ Laufzeitfehler: ' + (err && err.stack ? err.stack : (err && err.message ? err.message : String(err))) + '\\n' });
+ self.postMessage({ type: 'done', success: false, error: err ? (err.message || String(err)) : 'Fehler' });
+ });
+ } else {
+ self.postMessage({ type: 'done', success: true });
+ }
} catch (err) {
self.postMessage({ type: 'stderr', text: '\\n❌ Laufzeitfehler: ' + err.stack + '\\n' });
self.postMessage({ type: 'done', success: false, error: err.message });
@@ -152,6 +163,32 @@
throw new Error(msg || 'Bedingung ist nicht wahr');
}
passed++;
+ },
+ async rejects(promiseOrFn, expectedMsg, msg) {
+ total++;
+ let threw = false;
+ let caughtError = null;
+ try {
+ if (typeof promiseOrFn === 'function') {
+ await promiseOrFn();
+ } else {
+ await promiseOrFn;
+ }
+ } catch (err) {
+ threw = true;
+ caughtError = err;
+ }
+ if (!threw) {
+ failures++;
+ throw new Error(msg || 'Promise sollte abgelehnt werden (reject), wurde aber erfolgreich aufgelöst.');
+ }
+ if (expectedMsg && typeof expectedMsg === 'string') {
+ if (!String(caughtError && (caughtError.message || caughtError)).includes(expectedMsg)) {
+ failures++;
+ throw new Error(msg || ('Erwartete Fehlermeldung "' + expectedMsg + '" nicht gefunden in: ' + caughtError));
+ }
+ }
+ passed++;
}
};
@@ -159,9 +196,9 @@
${userCode}
// 2. Test-Suite ausführen
- ${testCode}
-
- ({ total, passed, failures });
+ return (async () => {
+ ${testCode}
+ })();
`;
this.runCode(fullScript, logCallback).then((res) => {
diff --git a/courses/javascript/01_erste_schritte_variablen/index.html b/courses/javascript/01_erste_schritte_variablen/index.html
index 030de37..e9c8f41 100644
--- a/courses/javascript/01_erste_schritte_variablen/index.html
+++ b/courses/javascript/01_erste_schritte_variablen/index.html
@@ -45,8 +45,9 @@ console.log(text);
TODO 3: Funktion istVolljaehrig(alter) schreiben.
-
+
diff --git a/courses/javascript/03_objekte_und_destructuring/README.md b/courses/javascript/03_objekte_und_destructuring/README.md
new file mode 100644
index 0000000..53e9175
--- /dev/null
+++ b/courses/javascript/03_objekte_und_destructuring/README.md
@@ -0,0 +1,118 @@
+# JS 03: Objekte, Destructuring & Rest/Spread 📦
+
+Willkommen zu **Modul 03** des JavaScript-Kurses!
+
+Objekte sind der Herzschlag von JavaScript: Fast alles in JavaScript ist ein Objekt oder verhält sich wie eines. In diesem Modul lernst du, wie du strukturierte Daten erstellst, mit modernem **ES6-Destructuring** blitzschnell entpackst und Daten mit dem **Spread- & Rest-Operator (`...`)** elegant transformierst.
+
+---
+
+## 💡 1. Das Wichtigste in Kürze
+
+### A. Object Literals & Property Shorthand
+Ein Objekt speichert Daten als **Schlüssel-Wert-Paare** (*Key-Value Pairs*):
+
+```javascript
+// Klassische Deklaration
+const person = {
+ name: "Max",
+ alter: 25,
+ istEntwickler: true
+};
+
+// ES6 Property Shorthand (Wenn Variablenname und Key identisch sind)
+const name = "Anna";
+const email = "anna@example.com";
+const user = { name, email };
+// Entspricht: { name: name, email: email }
+```
+
+### B. Destructuring Assignment (Objekte & Arrays entpacken)
+Mit Destructuring kannst du Werte direkt aus Objekten oder Arrays extrahieren, ohne wiederholt `objekt.eigenschaft` schreiben zu müssen:
+
+```javascript
+const dev = { name: "Sarah", sprache: "JavaScript", erfahrung: 5 };
+
+// 🎯 Object Destructuring:
+const { name, sprache } = dev;
+console.log(name); // "Sarah"
+console.log(sprache); // "JavaScript"
+
+// 🎯 Alias (Umbennen) & Standardwerte (Default Values):
+const { alter = 18, name: vorname } = dev;
+console.log(alter); // 18 (Default-Wert, da 'alter' im Objekt fehlt)
+console.log(vorname); // "Sarah" (unter neuem Namen verfügbar)
+
+// 🎯 Array Destructuring:
+const farben = ["#ff0000", "#00ff00", "#0000ff"];
+const [rot, gruen] = farben;
+console.log(rot); // "#ff0000"
+```
+
+### C. Spread- & Rest-Operator (`...`)
+Die drei Punkte `...` haben je nach Kontext zwei mächtige Funktionen:
+
+1. **Spread-Operator (Ausbreiten / Zusammenfügen):**
+ Erzeugt eine flache Kopie (*Shallow Copy*) oder kombiniert mehrere Objekte/Arrays ohne Mutation:
+ ```javascript
+ const basisInfo = { name: "Tom", alter: 30 };
+ const kontakt = { email: "tom@web.de", stadt: "Berlin" };
+
+ // Neues Objekt mit allen Eigenschaften beider Objekte:
+ const vollstaendig = { ...basisInfo, ...kontakt, premium: true };
+ ```
+
+2. **Rest-Operator (Einsammeln des Rests):**
+ Sammelt verbleibende Eigenschaften in einem neuen Objekt/Array:
+ ```javascript
+ const { email, ...andereDaten } = vollstaendig;
+ console.log(email); // "tom@web.de"
+ console.log(andereDaten); // { name: "Tom", alter: 30, stadt: "Berlin", premium: true }
+ ```
+
+### D. Hilfsmethoden: `Object.keys()`, `Object.values()`, `Object.entries()`
+```javascript
+const auto = { marke: "Tesla", modell: "Model 3", baujahr: 2024 };
+
+console.log(Object.keys(auto)); // ["marke", "modell", "baujahr"]
+console.log(Object.values(auto)); // ["Tesla", "Model 3", 2024]
+console.log(Object.entries(auto)); // [["marke", "Tesla"], ["modell", "Model 3"], ["baujahr", 2024]]
+console.log(Object.keys(auto).length); // 3 (Anzahl der Eigenschaften)
+```
+
+---
+
+## 🧰 Die didaktische Analogie: "Die Werkzeugkiste & Auspack-Station"
+
+- **Das Objekt als Werkzeugkiste:**
+ Ein Objekt `{ hammer: "500g", saege: "Holz", bohrer: "Akku" }` ist wie eine strukturierte Werkzeugkiste mit beschrifteten Fächern.
+- **Destructuring als gezieltes Herausgreifen:**
+ Statt die ganze schwere Kiste an die Baustelle zu tragen (`werkzeugkiste.hammer`), greifst du mit `const { hammer, saege } = werkzeugkiste;` genau die Werkzeuge heraus, die du für die aktuelle Arbeit brauchst.
+- **Spread (`...`) als Zusammenlegen in eine neue Kiste:**
+ Mit `{ ...kiste1, ...kiste2, multimeter: "digital" }` schüttest du zwei Werkzeugkisten in eine neue, größere Kiste um und packst noch ein neues Werkzeug dazu – die ursprünglichen Kisten bleiben unberührt (*Immutability*)!
+
+---
+
+## 🎯 Aufgaben & Teilziele in `aufgabe.js`
+
+Öffne die Datei `aufgabe.js` und löse die 5 Teilziele:
+
+1. **TODO 1:** `erstelleBenutzer(name, email, rolle = "user")`
+ Erstelle und gib ein Objekt mit `{ name, email, rolle }` unter Verwendung des Property Shorthands zurück.
+
+2. **TODO 2:** `extrahiereDetails(benutzerObjekt)`
+ Extrahiere `name` und `email` mit Destructuring und gib den String `"${name} (${email})"` zurück.
+
+3. **TODO 3:** `fuegeAdresseHinzu(benutzerObjekt, adresseObjekt)`
+ Erzeuge mit Spread (`...`) ein neues, zusammengeführtes Objekt `{ ...benutzerObjekt, ...adresseObjekt }`, ohne `benutzerObjekt` zu verändern.
+
+4. **TODO 4:** `zaehleEigenschaften(objekt)`
+ Ermittle die Anzahl der Eigenschaften eines Objekts mit `Object.keys(objekt).length`.
+
+5. **TODO 5:** `berechneWarenkorbGesamtwert(warenkorbItems)`
+ Berechne die Summe aus `preis * anzahl` für alle Items im Array. Nutze Destructuring (`const { preis, anzahl = 1 }`) im Schleifendurchlauf oder in `.reduce()`.
+
+---
+
+## 🧪 Tests ausführen
+
+Führe den Test in der interaktiven Web-IDE oder im Runner aus. Deine Lösung ist perfekt, wenn alle Tests erfolgreich durchlaufen!
diff --git a/courses/javascript/03_objekte_und_destructuring/aufgabe.js b/courses/javascript/03_objekte_und_destructuring/aufgabe.js
new file mode 100644
index 0000000..d77196a
--- /dev/null
+++ b/courses/javascript/03_objekte_und_destructuring/aufgabe.js
@@ -0,0 +1,46 @@
+/**
+ * 🌐 JS 03: OBJEKTE, DESTRUCTURING & REST/SPREAD 🌐
+ * ==================================================
+ * Strukturierte Daten & moderne ES6-Operatoren in JavaScript.
+ */
+
+// 🎯 TEILZIEL 1 (TODO 1): erstelleBenutzer(name, email, rolle = "user")
+// Erstelle ein Benutzerobjekt mit Property Shorthand und Default-Parameter.
+// Rückgabe: Ein Objekt mit { name, email, rolle }
+function erstelleBenutzer(name, email, rolle = "user") {
+ // TODO: Nutze ES6 Property Shorthand { name, email, rolle }
+ return null;
+}
+
+// 🎯 TEILZIEL 2 (TODO 2): extrahiereDetails(benutzerObjekt)
+// Extrahiere 'name' und 'email' mittels Object Destructuring: const { name, email } = benutzerObjekt;
+// Rückgabe: Formatierter String im Format: `${name} (${email})`
+function extrahiereDetails(benutzerObjekt) {
+ // TODO: Destructuring anwenden und String im Format "Name (email@domain.de)" zurückgeben
+ return "";
+}
+
+// 🎯 TEILZIEL 3 (TODO 3): fuegeAdresseHinzu(benutzerObjekt, adresseObjekt)
+// Kombiniere Benutzer und Adresse zu einem NEUEN Objekt mittels Spread-Operator: { ...benutzerObjekt, ...adresseObjekt }
+// Wichtig: Das Originalobjekt benutzerObjekt darf NICHT verändert werden (Immutability).
+function fuegeAdresseHinzu(benutzerObjekt, adresseObjekt) {
+ // TODO: Neues Objekt mit Object Spread { ...benutzerObjekt, ...adresseObjekt } erstellen
+ return null;
+}
+
+// 🎯 TEILZIEL 4 (TODO 4): zaehleEigenschaften(objekt)
+// Zähle die Anzahl der eigenen Schlüssel/Eigenschaften eines Objekts mit Object.keys(objekt).length.
+// Bei leeren oder ungültigen Objekten soll 0 zurückgegeben werden.
+function zaehleEigenschaften(objekt) {
+ // TODO: Object.keys() nutzen und Anzahl der Keys zurückgeben
+ return 0;
+}
+
+// 🎯 TEILZIEL 5 (TODO 5): berechneWarenkorbGesamtwert(warenkorbItems)
+// Berechne die Summe aller (preis * anzahl) im Array warenkorbItems.
+// Nutze Destructuring im Loop oder in reduce: for (const { preis, anzahl = 1 } of warenkorbItems)
+// Bei leerem Warenkorb [] soll 0 zurückgegeben werden.
+function berechneWarenkorbGesamtwert(warenkorbItems) {
+ // TODO: Destructuring im Schleifendurchlauf oder reduce nutzen
+ return 0;
+}
diff --git a/courses/javascript/03_objekte_und_destructuring/index.html b/courses/javascript/03_objekte_und_destructuring/index.html
new file mode 100644
index 0000000..5f8f705
--- /dev/null
+++ b/courses/javascript/03_objekte_und_destructuring/index.html
@@ -0,0 +1,171 @@
+
+
+
+
+
+
JS 03: Objekte, Destructuring & Rest/Spread
+
+
+
+
+
+
+
+
+
+
+ 💡 1. Object Literals & Property Shorthand
+ Objekte speichern zusammengehörige Informationen als Schlüssel-Wert-Paare. Modernes JavaScript (ES6) bietet mit dem Property Shorthand eine extrem prägnante Syntax, wenn Variablen- und Eigenschaftsname übereinstimmen:
+
+ const name = "Max Mustermann";
+const email = "max@beispiel.de";
+
+// Vor ES6 (umständlich):
+const alterUser = { name: name, email: email };
+
+// Mit modernem ES6 Property Shorthand (sauber & prägnant):
+const benutzer = { name, email, rolle: "user" };
+console.log(benutzer); // { name: "Max Mustermann", email: "max@beispiel.de", rolle: "user" }
+
+
+
+
+ 📦 2. Destructuring Assignment (Auspacken von Daten)
+ Mit Destructuring entpackst du Werte direkt in separate Variablen. Das spart Code und erhöht die Lesbarkeit spürbar:
+
+ const person = { vorname: "Anna", stadt: "Berlin", punkte: 95 };
+
+// 🎯 Object Destructuring:
+const { vorname, stadt } = person;
+console.log(vorname); // "Anna"
+
+// 🎯 Destructuring mit Standardwerten und Aliases (Umbenennung):
+const { land = "Deutschland", punkte: score } = person;
+console.log(land); // "Deutschland" (Default, da 'land' in person nicht existiert)
+console.log(score); // 95 (Alias für person.punkte)
+
+// 🎯 Array Destructuring:
+const koordinaten = [52.52, 13.405];
+const [breite, laenge] = koordinaten;
+console.log(breite, laenge); // 52.52, 13.405
+
+
+
+
+ ✨ 3. Spread- & Rest-Operator (...)
+ Die drei Punkte (...) ermöglichen das Ausbreiten (Spread) und Einsammeln (Rest) von Datenstrukturen:
+
+ const basisProfil = { name: "Lisa", alter: 28 };
+const kontaktInfo = { email: "lisa@dev.io", stadt: "Hamburg" };
+
+// 🌟 Spread: Objekte verschmelzen ohne Mutation (Immutability):
+const vollstaendigesProfil = { ...basisProfil, ...kontaktInfo, status: "aktiv" };
+console.log(vollstaendigesProfil);
+// { name: "Lisa", alter: 28, email: "lisa@dev.io", stadt: "Hamburg", status: "aktiv" }
+
+// 🌟 Rest: Eigenschaften separieren:
+const { email, ...restlicheDaten } = vollstaendigesProfil;
+console.log(email); // "lisa@dev.io"
+console.log(restlicheDaten); // { name: "Lisa", alter: 28, stadt: "Hamburg", status: "aktiv" }
+
+
+
🧰 Didaktische Analogie: Die Werkzeugkiste & Auspack-Station
+
Ein Objekt ist wie eine beschriftete Werkzeugkiste . Destructuring ist das gezielte Herausgreifen von genau dem Werkzeug (z. B. Hammer und Zange), das du jetzt benötigst, ohne die ganze Kiste herumtragen zu müssen. Der Spread-Operator (...) ist wie das Umschütten zweier Kisten in einen neuen Koffer, ohne den Originalinhalt zu beschädigen.
+
+
+
+
+
+ 📑 4. Cheat-Sheet: Object & Destructuring Methoden
+
+
+
+
+ Syntax / Methode
+ Beispiel
+ Beschreibung
+
+
+
+
+ { a, b }
+ const obj = { name, email };
+ Property Shorthand
+
+
+ const { k } = obj
+ const { name, rolle = "user" } = u;
+ Object Destructuring mit Default-Wert
+
+
+ { ...obj, neu }
+ const klon = { ...original, id: 42 };
+ Spread-Operator zum Klonen & Erweitern
+
+
+ Object.keys(obj)
+ Object.keys({ a: 1, b: 2 }) // ["a", "b"]
+ Array aller Eigenschaftsnamen (Keys)
+
+
+ Object.values(obj)
+ Object.values({ a: 1, b: 2 }) // [1, 2]
+ Array aller Werte (Values)
+
+
+ Object.entries(obj)
+ Object.entries({ a: 1 }) // [["a", 1]]
+ Array aus [key, value]-Paaren
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/courses/javascript/03_objekte_und_destructuring/musterloesung.js b/courses/javascript/03_objekte_und_destructuring/musterloesung.js
new file mode 100644
index 0000000..a100dd1
--- /dev/null
+++ b/courses/javascript/03_objekte_und_destructuring/musterloesung.js
@@ -0,0 +1,27 @@
+/**
+ * 🌟 MUSTERLÖSUNG: JS 03 – OBJEKTE & DESTRUCTURING 🌟
+ * ==================================================
+ */
+
+function erstelleBenutzer(name, email, rolle = "user") {
+ return { name, email, rolle };
+}
+
+function extrahiereDetails(benutzerObjekt) {
+ const { name, email } = benutzerObjekt;
+ return `${name} (${email})`;
+}
+
+function fuegeAdresseHinzu(benutzerObjekt, adresseObjekt) {
+ return { ...benutzerObjekt, ...adresseObjekt };
+}
+
+function zaehleEigenschaften(objekt) {
+ if (!objekt || typeof objekt !== "object") return 0;
+ return Object.keys(objekt).length;
+}
+
+function berechneWarenkorbGesamtwert(warenkorbItems) {
+ if (!Array.isArray(warenkorbItems) || warenkorbItems.length === 0) return 0;
+ return warenkorbItems.reduce((gesamt, { preis = 0, anzahl = 1 }) => gesamt + preis * anzahl, 0);
+}
diff --git a/courses/javascript/03_objekte_und_destructuring/test_aufgabe.js b/courses/javascript/03_objekte_und_destructuring/test_aufgabe.js
new file mode 100644
index 0000000..1c7b8ea
--- /dev/null
+++ b/courses/javascript/03_objekte_und_destructuring/test_aufgabe.js
@@ -0,0 +1,83 @@
+// 🧪 JS 03: Testsuite für Objekte, Destructuring & Rest/Spread
+
+// Test 1: erstelleBenutzer
+const user1 = erstelleBenutzer("Max Mustermann", "max@beispiel.de");
+assert.deepStrictEqual(
+ user1,
+ { name: "Max Mustermann", email: "max@beispiel.de", rolle: "user" },
+ "erstelleBenutzer() sollte Standard-Rolle 'user' und korrekte Attribute besitzen"
+);
+
+const adminUser = erstelleBenutzer("Alice Admin", "alice@corp.de", "admin");
+assert.deepStrictEqual(
+ adminUser,
+ { name: "Alice Admin", email: "alice@corp.de", rolle: "admin" },
+ "erstelleBenutzer() sollte benutzerdefinierte Rolle 'admin' unterstützen"
+);
+
+// Test 2: extrahiereDetails
+const details1 = extrahiereDetails({ name: "Anna Schmidt", email: "anna@web.de", rolle: "user" });
+assert.strictEqual(
+ details1,
+ "Anna Schmidt (anna@web.de)",
+ "extrahiereDetails() sollte 'Name (Email)' zurückgeben"
+);
+
+const details2 = extrahiereDetails({ name: "Bob", email: "bob@dev.io", alter: 30 });
+assert.strictEqual(
+ details2,
+ "Bob (bob@dev.io)",
+ "extrahiereDetails() sollte auch mit zusätzlichen Objekt-Eigenschaften funktionieren"
+);
+
+// Test 3: fuegeAdresseHinzu & Immutability Check
+const basisBenutzer = { name: "Sarah", email: "sarah@mail.de" };
+const adresse = { stadt: "Hamburg", plz: "20095" };
+const kombiniert = fuegeAdresseHinzu(basisBenutzer, adresse);
+
+assert.deepStrictEqual(
+ kombiniert,
+ { name: "Sarah", email: "sarah@mail.de", stadt: "Hamburg", plz: "20095" },
+ "fuegeAdresseHinzu() sollte Benutzer und Adresse zu einem neuen Objekt verschmelzen"
+);
+assert.strictEqual(
+ basisBenutzer.stadt,
+ undefined,
+ "fuegeAdresseHinzu() darf das Originalobjekt nicht mutieren (Immutability)"
+);
+
+// Test 4: zaehleEigenschaften
+assert.strictEqual(zaehleEigenschaften({ a: 1, b: 2, c: 3 }), 3, "zaehleEigenschaften({a:1, b:2, c:3}) sollte 3 ergeben");
+assert.strictEqual(zaehleEigenschaften({}), 0, "zaehleEigenschaften({}) für leeres Objekt sollte 0 sein");
+assert.strictEqual(
+ zaehleEigenschaften({ id: 101, title: "Laptop", preis: 999, verfuegbar: true }),
+ 4,
+ "zaehleEigenschaften() sollte 4 Eigenschaften korrekt zählen"
+);
+
+// Test 5: berechneWarenkorbGesamtwert
+const warenkorb = [
+ { titel: "Buch", preis: 20, anzahl: 2 },
+ { titel: "Maus", preis: 15, anzahl: 3 },
+ { titel: "Kabel", preis: 5, anzahl: 1 }
+];
+assert.strictEqual(
+ berechneWarenkorbGesamtwert(warenkorb),
+ 90,
+ "berechneWarenkorbGesamtwert() sollte 20*2 + 15*3 + 5*1 = 90 berechnen"
+);
+
+const warenkorbMitDefault = [{ titel: "Monitor", preis: 199 }];
+assert.strictEqual(
+ berechneWarenkorbGesamtwert(warenkorbMitDefault),
+ 199,
+ "berechneWarenkorbGesamtwert() sollte anzahl = 1 als Fallback nutzen"
+);
+
+assert.strictEqual(
+ berechneWarenkorbGesamtwert([]),
+ 0,
+ "berechneWarenkorbGesamtwert([]) für leeren Warenkorb sollte 0 sein"
+);
+
+console.log("✅ Alle 10 Tests in JS 03 erfolgreich bestanden!");
diff --git a/courses/javascript/04_dom_und_events/README.md b/courses/javascript/04_dom_und_events/README.md
new file mode 100644
index 0000000..4c52550
--- /dev/null
+++ b/courses/javascript/04_dom_und_events/README.md
@@ -0,0 +1,143 @@
+# JS 04: DOM-Manipulation & Events 🎭
+
+Willkommen zu **Modul 04** des JavaScript-Kurses!
+
+Das **Document Object Model (DOM)** ist die Schnittstelle zwischen JavaScript und der sichtbaren HTML-Struktur im Webbrowser. Durch die Kombination aus DOM-Selektoren, Manipulation und Event-Listenern hauchst du statischen Webseiten echtes Leben ein und machst sie interaktiv.
+
+---
+
+## 💡 1. Das Wichtigste in Kürze
+
+### A. Was ist das DOM?
+Der Browser parst HTML-Code und baut daraus einen hierarchischen Baum aus JavaScript-Objekten (*DOM Tree*):
+
+```
+ document
+ │
+
+ ┌───┴───┐
+
+ │ ┌───┴────────┐
+
(Container)
+ ┌───┴───┐
+
+ │
+
+```
+
+Jeder HTML-Tag wird zu einem **DOM-Element (Node)** mit Eigenschaften und Methoden, die du in JavaScript zur Laufzeit auslesen und verändern kannst.
+
+---
+
+### B. Elemente selektieren
+Moderne Selektoren nutzen dieselbe Syntax wie CSS:
+
+```javascript
+// Einzelnes Element selektieren (erstes Vorkommen):
+const ueberschrift = document.querySelector("h1");
+const speicherBtn = document.querySelector("#save-btn");
+const erstesItem = document.querySelector(".liste-item");
+
+// Mehrere Elemente selektieren (NodeList / Collection):
+const alleButtons = document.querySelectorAll(".btn");
+alleButtons.forEach(btn => console.log(btn.textContent));
+```
+
+---
+
+### C. Element-Inhalte & Klassen manipulieren
+```javascript
+const box = document.querySelector("#info-box");
+
+// 1. Textinhalt sicher ändern (schützt vor XSS-Sicherheitslücken):
+box.textContent = "Neuer sicherer Text";
+
+// 2. CSS-Klassen verwalten mit classList:
+box.classList.add("aktiv"); // Klasse hinzufügen
+box.classList.remove("inaktiv"); // Klasse entfernen
+box.classList.toggle("sichtbar"); // Hinzufügen falls fehlt, sonst entfernen
+console.log(box.classList.contains("aktiv")); // true oder false
+```
+
+---
+
+### D. Event-Listener (Ereignis-Steuerung)
+Mit `addEventListener()` reagiert deine Webanwendung auf Benutzerinteraktionen wie Klicks, Tastatureingaben oder Formulare:
+
+```javascript
+const button = document.querySelector("#klick-mich");
+
+button.addEventListener("click", (event) => {
+ console.log("Button wurde angeklickt!", event.target);
+ button.textContent = "Erfolgreich geklickt! 🎉";
+});
+```
+
+---
+
+### E. Dynamische Elemente erstellen
+Du kannst neue HTML-Elemente dynamisch zur Laufzeit erzeugen und in den DOM-Baum einhängen:
+
+```javascript
+// 1. Element im Speicher erzeugen:
+const neuesLi = document.createElement("li");
+neuesLi.textContent = "Neuer dynamischer Eintrag";
+neuesLi.classList.add("eintrag");
+
+// 2. In den DOM-Baum an ein Elternelement anfügen:
+const liste = document.querySelector("#aufgaben-liste");
+liste.appendChild(neuesLi);
+```
+
+---
+
+## 🎼 Die didaktische Analogie: "Der Dirigent des Web-Orchesters"
+
+- **Das HTML-Dokument als Partitur:**
+ HTML liefert den statischen Notentext und die Struktur des Musikstücks.
+- **Der Browser als Orchester:**
+ Die Musiker (DOM-Nodes) sitzen bereit, um Töne und Farben darzustellen.
+- **JavaScript als Dirigent:**
+ - **Selektoren (`querySelector`):** Der Dirigent schaut gezielt zu den Streichern oder Bläsern.
+ - **Manipulation (`textContent`, `classList`):** Mit dem Taktstock gibt der Dirigent Anweisungen für Lautstärke, Helligkeit oder Text.
+ - **Events (`addEventListener`):** Der Dirigent wartet auf den Einsatz oder den Applaus des Publikums und reagiert dynamisch.
+
+---
+
+## 🏗️ Headless- & Isolierte Logik (Testbare Architektur)
+
+In moderner Web-Entwicklung (wie React, Vue oder Node.js) trennen professionelle Entwickler reine **Berechnungs- und Zustandslogik** von direkten DOM-Zugriffen.
+
+Dadurch können Funktionen wie `aktualisiereZaehlerAnzeige` oder `filtereElementeNachKlasse` sowohl im echten Browser als auch in **Headless-Umgebungen (z.B. Unittests im Web-Worker / CI-Runner)** blitzschnell und ohne echten Monitor getestet werden!
+
+---
+
+## 🎯 Aufgaben & Teilziele in `aufgabe.js`
+
+Öffne `aufgabe.js` und löse die 4 Aufgaben:
+
+1. **TODO 1:** `erzeugeButtonElement(text, cssKlasse, clickCallback)`
+ Erstelle einen Button (`document.createElement("button")`), setze den Text, füge optional die CSS-Klasse hinzu und binde den Click-Callback per `addEventListener` an.
+
+2. **TODO 2:** `erstelleListenEintrag(titel, istErledigt = false)`
+ Erstelle ein ` `-Element mit dem Titel und füge die Klasse `"erledigt"` hinzu, falls `istErledigt === true` ist.
+
+3. **TODO 3:** `filtereElementeNachKlasse(elementListe, cssKlasse)`
+ Filtere ein Array von DOM-Elementen nach `element.classList.contains(cssKlasse)`.
+
+4. **TODO 4:** `aktualisiereZaehlerAnzeige(aktuellerWert, schwellenwert = 10)`
+ Berechne das Zustands-Objekt für eine Zähler-Komponente:
+ ```javascript
+ {
+ text: `Zähler: ${aktuellerWert} / ${schwellenwert}`,
+ wert: aktuellerWert,
+ istKritisch: aktuellerWert >= schwellenwert,
+ statusKlasse: aktuellerWert >= schwellenwert ? "status-kritisch" : "status-normal"
+ }
+ ```
+
+---
+
+## 🧪 Tests ausführen
+
+Starte die Tests im Test-Runner oder in der Web-IDE. Sobald alle Tests grün sind, hast du DOM-Manipulation & Events gemeistert!
diff --git a/courses/javascript/04_dom_und_events/aufgabe.js b/courses/javascript/04_dom_und_events/aufgabe.js
new file mode 100644
index 0000000..86d9d27
--- /dev/null
+++ b/courses/javascript/04_dom_und_events/aufgabe.js
@@ -0,0 +1,48 @@
+/**
+ * 🌐 JS 04: DOM-MANIPULATION & EVENTS 🌐
+ * ========================================
+ * Interaktive Webseiten & Event-Driven Architecture in JavaScript.
+ */
+
+// 🎯 TEILZIEL 1 (TODO 1): erzeugeButtonElement(text, cssKlasse, clickCallback)
+// Erzeuge ein Button-Element mit document.createElement("button").
+// 1. Setze textContent auf 'text'.
+// 2. Füge 'cssKlasse' über classList.add(cssKlasse) hinzu (falls cssKlasse übergeben wurde).
+// 3. Registriere 'clickCallback' mit button.addEventListener("click", clickCallback) (falls clickCallback eine Funktion ist).
+// Rückgabe: Das fertige Button-Element
+function erzeugeButtonElement(text, cssKlasse, clickCallback) {
+ // TODO: Button erstellen, Text setzen, Klasse hinzufügen, Event-Listener registrieren
+ return null;
+}
+
+// 🎯 TEILZIEL 2 (TODO 2): erstelleListenEintrag(titel, istErledigt = false)
+// Erzeuge ein -Element mit document.createElement("li").
+// 1. Setze textContent auf 'titel'.
+// 2. Wenn istErledigt === true ist, füge die CSS-Klasse "erledigt" hinzu (classList.add("erledigt")).
+// Rückgabe: Das erstellte -Element
+function erstelleListenEintrag(titel, istErledigt = false) {
+ // TODO: erzeugen, Text setzen, optional Klasse "erledigt" hinzufügen
+ return null;
+}
+
+// 🎯 TEILZIEL 3 (TODO 3): filtereElementeNachKlasse(elementListe, cssKlasse)
+// Filtert ein Array oder eine NodeList von Elementen.
+// Rückgabe: Ein neues Array, das nur Elemente enthält, bei denen element.classList.contains(cssKlasse) true ist.
+function filtereElementeNachKlasse(elementListe, cssKlasse) {
+ // TODO: Array.from(elementListe).filter(...) mit classList.contains(cssKlasse)
+ return [];
+}
+
+// 🎯 TEILZIEL 4 (TODO 4): aktualisiereZaehlerAnzeige(aktuellerWert, schwellenwert = 10)
+// Berechnet den Status einer Zähleranzeige (Headless-UI State Pattern):
+// Rückgabe-Objekt:
+// {
+// text: `Zähler: ${aktuellerWert} / ${schwellenwert}`,
+// wert: aktuellerWert,
+// istKritisch: aktuellerWert >= schwellenwert,
+// statusKlasse: aktuellerWert >= schwellenwert ? "status-kritisch" : "status-normal"
+// }
+function aktualisiereZaehlerAnzeige(aktuellerWert, schwellenwert = 10) {
+ // TODO: Zählerstatus berechnen und Objekt zurückgeben
+ return null;
+}
diff --git a/courses/javascript/04_dom_und_events/index.html b/courses/javascript/04_dom_und_events/index.html
new file mode 100644
index 0000000..53e7bee
--- /dev/null
+++ b/courses/javascript/04_dom_und_events/index.html
@@ -0,0 +1,164 @@
+
+
+
+
+
+ JS 04: DOM-Manipulation & Events
+
+
+
+
+
+
+
+
+
+
+ 🌳 1. Der DOM-Baum & Selektoren
+ Der Browser übersetzt HTML in eine Baumstruktur von Objekten (den DOM-Baum ). Mit modernen Selektoren greifst du gezielt auf jedes Element zu:
+
+
+ document
+ └── <html>
+ ├── <head> (title, meta, link)
+ └── <body>
+ ├── <h1 id="titel">
+ ├── <button class="btn">
+ └── <ul id="aufgaben"> ── <li class="erledigt">
+
+
+ // 🎯 Einzelnes Element mit CSS-Selektor ansprechen:
+const titel = document.querySelector("#titel");
+const button = document.querySelector(".btn-primary");
+
+// 🎯 Alle passenden Elemente als NodeList abfragen:
+const alleItems = document.querySelectorAll("ul#aufgaben > li");
+alleItems.forEach(item => console.log(item.textContent));
+
+
+
+
+ 🎨 2. Elemente manipulieren & stylen
+ Über Eigenschaften wie textContent und classList veränderst du Texte und Klassen zur Laufzeit:
+
+ const karte = document.querySelector(".info-karte");
+
+// 🔒 Sicherer Text (verhindert Cross-Site-Scripting / XSS):
+karte.textContent = "Aktualisierter Status: Online";
+
+// 🏷️ CSS-Klassen verwalten mit classList:
+karte.classList.add("aktiv"); // Klasse hinzufügen
+karte.classList.remove("inaktiv"); // Klasse entfernen
+karte.classList.toggle("hervorgehoben"); // Ein- oder ausschalten
+console.log(karte.classList.contains("aktiv")); // true
+
+
+
+
+ ⚡ 3. Event-Driven Architecture (Reaktivität)
+ Mit addEventListener() reagiert deine Website sofort auf Aktionen des Nutzers (Klicks, Tastenanschläge, Formularabsendungen):
+
+ const saveBtn = document.querySelector("#btn-save");
+
+saveBtn.addEventListener("click", (event) => {
+ console.log("Button geklickt! Ziel-Element:", event.target);
+ saveBtn.textContent = "Gespeichert! ✅";
+ saveBtn.classList.add("btn-success");
+});
+
+
+
🎼 Didaktische Analogie: Der Dirigent des Web-Orchesters
+
HTML ist die Partitur , der Browser das Orchester und dein JavaScript-Code der Dirigent : Selektoren sind der gezielte Blick zu den Instrumentengruppen, DOM-Manipulation ist die Taktstockbewegung für Lautstärke oder Klang, und Event-Listener sind das aufmerksame Lauschen auf den richtigen Takt und Applaus.
+
+
+
+
+
+ 📑 4. Cheat-Sheet: DOM-Methoden & Events
+
+
+
+
+ Methode / Eigenschaft
+ Beispiel
+ Wirkung
+
+
+
+
+ document.querySelector(sel)
+ document.querySelector(".btn")
+ Findet das erste passende Element
+
+
+ document.querySelectorAll(sel)
+ document.querySelectorAll("li")
+ Findet alle passenden Elemente
+
+
+ document.createElement(tag)
+ const btn = document.createElement("button")
+ Erstellt ein neues Element im Speicher
+
+
+ element.textContent
+ el.textContent = "Hallo Welt"
+ Liest oder setzt reinen Text
+
+
+ element.classList.add/remove/contains
+ el.classList.contains("aktiv")
+ Manipuliert und prüft CSS-Klassen
+
+
+ element.addEventListener(ev, fn)
+ el.addEventListener("click", () => ...)
+ Registriert Callback bei Ereignis
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/courses/javascript/04_dom_und_events/musterloesung.js b/courses/javascript/04_dom_und_events/musterloesung.js
new file mode 100644
index 0000000..62e65b8
--- /dev/null
+++ b/courses/javascript/04_dom_und_events/musterloesung.js
@@ -0,0 +1,40 @@
+/**
+ * 🌟 MUSTERLÖSUNG: JS 04 – DOM & EVENTS 🌟
+ * ========================================
+ */
+
+function erzeugeButtonElement(text, cssKlasse, clickCallback) {
+ const button = document.createElement("button");
+ button.textContent = text;
+ if (cssKlasse) {
+ button.classList.add(cssKlasse);
+ }
+ if (typeof clickCallback === "function") {
+ button.addEventListener("click", clickCallback);
+ }
+ return button;
+}
+
+function erstelleListenEintrag(titel, istErledigt = false) {
+ const li = document.createElement("li");
+ li.textContent = titel;
+ if (istErledigt) {
+ li.classList.add("erledigt");
+ }
+ return li;
+}
+
+function filtereElementeNachKlasse(elementListe, cssKlasse) {
+ const arr = Array.isArray(elementListe) ? elementListe : Array.from(elementListe || []);
+ return arr.filter(el => el && el.classList && el.classList.contains(cssKlasse));
+}
+
+function aktualisiereZaehlerAnzeige(aktuellerWert, schwellenwert = 10) {
+ const istKritisch = aktuellerWert >= schwellenwert;
+ return {
+ text: `Zähler: ${aktuellerWert} / ${schwellenwert}`,
+ wert: aktuellerWert,
+ istKritisch,
+ statusKlasse: istKritisch ? "status-kritisch" : "status-normal"
+ };
+}
diff --git a/courses/javascript/04_dom_und_events/test_aufgabe.js b/courses/javascript/04_dom_und_events/test_aufgabe.js
new file mode 100644
index 0000000..a6de201
--- /dev/null
+++ b/courses/javascript/04_dom_und_events/test_aufgabe.js
@@ -0,0 +1,127 @@
+// 🧪 JS 04: Testsuite für DOM-Manipulation & Events
+
+// Mock-DOM Umgebung für Headless / Web-Worker Ausführung
+if (typeof document === "undefined") {
+ globalThis.document = {
+ createElement(tagName) {
+ const classes = new Set();
+ const listeners = {};
+ const el = {
+ tagName: String(tagName).toUpperCase(),
+ textContent: "",
+ innerHTML: "",
+ get className() {
+ return Array.from(classes).join(" ");
+ },
+ set className(val) {
+ classes.clear();
+ if (val) String(val).trim().split(/\s+/).forEach(c => c && classes.add(c));
+ },
+ classList: {
+ add(...tokens) {
+ tokens.forEach(t => t && classes.add(String(t)));
+ },
+ remove(...tokens) {
+ tokens.forEach(t => classes.delete(String(t)));
+ },
+ toggle(token) {
+ if (classes.has(token)) {
+ classes.delete(token);
+ return false;
+ } else {
+ classes.add(token);
+ return true;
+ }
+ },
+ contains(token) {
+ return classes.has(token);
+ }
+ },
+ addEventListener(event, callback) {
+ if (!listeners[event]) listeners[event] = [];
+ listeners[event].push(callback);
+ },
+ dispatchEvent(eventObj) {
+ const type = typeof eventObj === "string" ? eventObj : eventObj.type;
+ const list = listeners[type] || [];
+ const ev = typeof eventObj === "string" ? { type, target: this } : eventObj;
+ list.forEach(cb => cb(ev));
+ },
+ click() {
+ this.dispatchEvent({ type: "click", target: this });
+ }
+ };
+ return el;
+ }
+ };
+}
+
+// Test 1: erzeugeButtonElement
+let clicked = false;
+const btn1 = erzeugeButtonElement("Speichern", "btn-primary", () => { clicked = true; });
+
+assert.ok(btn1, "erzeugeButtonElement() sollte ein Element zurückgeben");
+assert.strictEqual(btn1.textContent, "Speichern", "Button textContent sollte 'Speichern' sein");
+assert.ok(btn1.classList.contains("btn-primary"), "Button sollte die Klasse 'btn-primary' besitzen");
+
+if (typeof btn1.click === "function") {
+ btn1.click();
+} else if (typeof btn1.dispatchEvent === "function") {
+ btn1.dispatchEvent({ type: "click" });
+}
+assert.strictEqual(clicked, true, "Button Click-Callback sollte beim Klick ausgelöst werden");
+
+// Test 1b: erzeugeButtonElement ohne optionale Klasse/Callback
+const btnSimple = erzeugeButtonElement("Abbrechen");
+assert.strictEqual(btnSimple.textContent, "Abbrechen", "Einfacher Button ohne Parameter sollte funktionieren");
+
+// Test 2: erstelleListenEintrag
+const liOffen = erstelleListenEintrag("Milch kaufen", false);
+assert.ok(liOffen, "erstelleListenEintrag() sollte ein Element zurückgeben");
+assert.strictEqual(liOffen.textContent, "Milch kaufen", "Listeneintrag-Text sollte 'Milch kaufen' sein");
+assert.strictEqual(liOffen.classList.contains("erledigt"), false, "Unerledigter Eintrag darf nicht die Klasse 'erledigt' haben");
+
+const liErledigt = erstelleListenEintrag("Post abholen", true);
+assert.strictEqual(liErledigt.textContent, "Post abholen", "Erledigter Eintrag Text sollte stimmen");
+assert.strictEqual(liErledigt.classList.contains("erledigt"), true, "Erledigter Eintrag muss die Klasse 'erledigt' besitzen");
+
+// Test 3: filtereElementeNachKlasse
+const el1 = document.createElement("div");
+el1.classList.add("aktiv");
+const el2 = document.createElement("div");
+el2.classList.add("inaktiv");
+const el3 = document.createElement("div");
+el3.classList.add("aktiv");
+el3.classList.add("hervorgehoben");
+
+const gefiltertAktiv = filtereElementeNachKlasse([el1, el2, el3], "aktiv");
+assert.strictEqual(gefiltertAktiv.length, 2, "filtereElementeNachKlasse() sollte 2 aktive Elemente finden");
+assert.strictEqual(gefiltertAktiv[0], el1, "Erstes gefiltertes Element sollte el1 sein");
+assert.strictEqual(gefiltertAktiv[1], el3, "Zweites gefiltertes Element sollte el3 sein");
+
+const gefiltertHervorgehoben = filtereElementeNachKlasse([el1, el2, el3], "hervorgehoben");
+assert.strictEqual(gefiltertHervorgehoben.length, 1, "Sollte 1 hervorgehobenes Element finden");
+
+// Test 4: aktualisiereZaehlerAnzeige
+const statusNormal = aktualisiereZaehlerAnzeige(5, 10);
+assert.deepStrictEqual(
+ statusNormal,
+ { text: "Zähler: 5 / 10", wert: 5, istKritisch: false, statusKlasse: "status-normal" },
+ "aktualisiereZaehlerAnzeige(5, 10) sollte Normalstatus liefern"
+);
+
+const statusKritisch = aktualisiereZaehlerAnzeige(10, 10);
+assert.deepStrictEqual(
+ statusKritisch,
+ { text: "Zähler: 10 / 10", wert: 10, istKritisch: true, statusKlasse: "status-kritisch" },
+ "aktualisiereZaehlerAnzeige(10, 10) bei Erreichen des Schwellenwerts sollte kritisch sein"
+);
+
+const statusUeberlimit = aktualisiereZaehlerAnzeige(15, 10);
+assert.strictEqual(statusUeberlimit.istKritisch, true, "Überlimit sollte istKritisch=true haben");
+assert.strictEqual(statusUeberlimit.statusKlasse, "status-kritisch", "Überlimit sollte 'status-kritisch' haben");
+
+const statusDefault = aktualisiereZaehlerAnzeige(3);
+assert.strictEqual(statusDefault.text, "Zähler: 3 / 10", "Standard-Schwellenwert sollte 10 sein");
+
+console.log("✅ Alle 14 Tests in JS 04 erfolgreich bestanden!");
diff --git a/courses/javascript/05_callbacks_und_promises/README.md b/courses/javascript/05_callbacks_und_promises/README.md
new file mode 100644
index 0000000..078c53b
--- /dev/null
+++ b/courses/javascript/05_callbacks_und_promises/README.md
@@ -0,0 +1,131 @@
+# 🌐 JS 05: Callbacks, Event Loop & Promises
+
+Willkommen zu **Modul 05** des JavaScript-Kurses! In diesem Kapitel lernst du das Herzstück der modernen Web-Entwicklung kennen: **Asynchrone Programmierung mit Promises**.
+
+---
+
+## 📖 1. Synchron vs. Asynchron & Der Event Loop
+
+JavaScript führt Code in einer einzigen Hauptausführungsschleife aus (**Single-Threaded**).
+
+- **Synchrone Ausführung (Blockierend):** Jede Codezeile wird nacheinander ausgeführt. Dauert eine Operation lange (z. B. ein schwerer Rechenschritt oder ein synchrones Warten), friert das gesamte Programm (und die Webseite!) ein.
+- **Asynchrone Ausführung (Nicht-blockierend):** Zeitintensive Aufgaben (z. B. Netzwerk-Anfragen, Timer oder Dateizugriffe) werden im Hintergrund angestoßen. JavaScript läuft währenddessen sofort weiter. Sobald das Ergebnis vorliegt, wird ein Callback in die Warteschlange gelegt und vom **Event Loop** abgearbeitet.
+
+```javascript
+console.log("1. Bestellung aufgeben");
+
+setTimeout(() => {
+ console.log("3. Essen ist fertig!"); // Asynchroner Callback nach 1000ms
+}, 1000);
+
+console.log("2. An den Tisch setzen und plaudern");
+// Ausgabe-Reihenfolge: 1 -> 2 -> 3
+```
+
+---
+
+## 🍔 2. Die Restaurant-Pieper-Analogie
+
+Stell dir vor, du bestellst Burger in einem Selbstbedienungs-Restaurant:
+
+1. **Bestellung:** Du gehst an die Kasse und bestellst dein Menü.
+2. **Der Pieper (`Promise` im Zustand `pending`):** Die Bedienung gibt dir sofort einen elektronischen Pieper in die Hand. Du musst *nicht* stur am Tresen stehen bleiben und die Schlange blockieren.
+3. **Nicht-blockierendes Warten:** Du setzt dich an deinen Tisch, surfst am Smartphone oder unterhältst dich mit Freunden (JavaScript Event Loop verarbeitet andere Ereignisse).
+4. **Erfolg (`resolve` -> `fulfilled`):** Der Pieper vibriert und leuchtet grün! Du holst dein frisches Essen ab (`.then(essen => ...)`).
+5. **Fehler (`reject` -> `rejected`):** Der Pieper piept rot und die Bedienung ruft: *"Die Burger-Patties sind leider aus!"* (`.catch(fehler => ...)`).
+
+---
+
+## ⏳ 3. Was ist ein Promise?
+
+Ein **Promise** (deutsch: *Versprechen*) ist ein Objekt, das einen Wert repräsentiert, der jetzt, in der Zukunft oder nie verfügbar ist.
+
+### Die 3 Zustände eines Promise:
+```
+ ┌───> fulfilled (erfolgreich gelöst) ──> .then(wert)
+ pending ──────┤
+ (schwebend) └───> rejected (fehlgeschlagen) ──> .catch(fehler)
+```
+
+1. **`pending` (schwebend):** Die asynchrone Operation läuft noch. Weder Erfolg noch Fehler.
+2. **`fulfilled` (erfüllt):** Die Operation war erfolgreich, `resolve(wert)` wurde aufgerufen.
+3. **`rejected` (abgewiesen):** Ein Fehler ist aufgetreten, `reject(error)` wurde aufgerufen.
+
+---
+
+## 🛠️ 4. Eigene Promises erstellen
+
+Ein Promise wird mit dem Konstruktor `new Promise(executor)` erzeugt. Die Executor-Funktion erhält zwei Parameter: `resolve` und `reject`.
+
+```javascript
+function simuliereDatenAbruf(erfolgreich) {
+ return new Promise((resolve, reject) => {
+ setTimeout(() => {
+ if (erfolgreich) {
+ resolve({ id: 1, name: "Max Mustermann" });
+ } else {
+ reject(new Error("Netzwerk-Timeout beim Laden!"));
+ }
+ }, 500);
+ });
+}
+```
+
+---
+
+## 🔗 5. Promise-Chaining: `.then()`, `.catch()`, `.finally()`
+
+Promises ermöglichen sauberes Verketten ohne verschachtelte Callback-Pyramiden (*Callback Hell*):
+
+```javascript
+simuliereDatenAbruf(true)
+ .then((daten) => {
+ console.log("Erhaltene Daten:", daten.name);
+ return daten.id; // Rückgabewert wird als nächstes Promise weitergegeben
+ })
+ .then((userId) => {
+ console.log("Lade Details für ID:", userId);
+ })
+ .catch((err) => {
+ console.error("Ein Fehler ist aufgetreten:", err.message);
+ })
+ .finally(() => {
+ console.log("Lade-Indikator ausblenden (wird immer ausgeführt)");
+ });
+```
+
+---
+
+## ⚡ 6. Parallele Ausführung mit `Promise.all()` und `Promise.race()`
+
+### `Promise.all([p1, p2, ...])`
+Führt mehrere Promises parallel aus und wartet, bis **alle** erfolgreich aufgelöst wurden.
+- Gibt ein Array mit allen Einzelergebnissen `[res1, res2, ...]` zurück.
+- **Fail-Fast:** Bricht sofort ab, sobald *auch nur ein einzelnes* Promise rejected!
+
+```javascript
+const p1 = fetchUser();
+const p2 = fetchPosts();
+
+Promise.all([p1, p2])
+ .then(([user, posts]) => {
+ console.log("User & Posts gleichzeitig geladen:", user, posts);
+ })
+ .catch(err => console.error("Mindestens eine Quelle schlug fehl:", err));
+```
+
+### `Promise.race([p1, p2, ...])`
+Liefert das Ergebnis des **allerschnellsten** Promise (egal ob `resolve` oder `reject`). Ideal z. B. für Timeouts.
+
+---
+
+## 🎯 7. Deine Aufgaben in `aufgabe.js`
+
+1. **`verzoegereAusfuehrung(ms, rueckgabeWert)`**:
+ Erzeuge ein Promise, das nach `ms` Millisekunden mittels `setTimeout` den `rueckgabeWert` auflöst.
+2. **`pruefeZahlGroesserNull(zahl)`**:
+ Prüfe die Zahl: Resolve bei `zahl > 0`, Reject mit `new Error("Zahl muss größer als 0 sein")` bei `<= 0`.
+3. **`ladeBenutzerProfil(userId)`**:
+ Asynchrones Profil-Laden: Resolve mit `{ id: userId, name: "Benutzer_" + userId, status: "aktiv" }` bei `userId > 0`, sonst Reject mit `new Error("Ungültige User-ID")`.
+4. **`ladeAlleDatenQuellen(quelleA, quelleB)`**:
+ Nutze `Promise.all([quelleA, quelleB])`, um zwei asynchrone Datenströme parallel zu aggregieren.
diff --git a/courses/javascript/05_callbacks_und_promises/aufgabe.js b/courses/javascript/05_callbacks_und_promises/aufgabe.js
new file mode 100644
index 0000000..a35e2a8
--- /dev/null
+++ b/courses/javascript/05_callbacks_und_promises/aufgabe.js
@@ -0,0 +1,44 @@
+/**
+ * 🌐 JS 05: CALLBACKS, EVENT LOOP & PROMISES 🌐
+ * =============================================
+ * Lerne die Grundlagen der asynchronen Programmierung in JavaScript:
+ * Callbacks, der Event Loop und Promises für nicht-blockierenden Code.
+ */
+
+// 🎯 TEILZIEL 1 (TODO 1): Schreibe verzoegereAusfuehrung(ms, rueckgabeWert)
+// Gibt ein neues Promise zurück, das nach `ms` Millisekunden mit `rueckgabeWert` aufgelöst (resolved) wird.
+// Nutze dafür setTimeout(callback, ms).
+function verzoegereAusfuehrung(ms, rueckgabeWert) {
+ // TODO: Gib ein new Promise((resolve) => ...) zurück
+ return null;
+}
+
+// 🎯 TEILZIEL 2 (TODO 2): Schreibe pruefeZahlGroesserNull(zahl)
+// Gibt ein Promise zurück:
+// - Wenn zahl > 0: resolve mit zahl
+// - Wenn zahl <= 0 oder keine Zahl: reject mit einem Error("Zahl muss größer als 0 sein")
+function pruefeZahlGroesserNull(zahl) {
+ // TODO: Gib ein Promise zurück, das resolve() oder reject() aufruft
+ return null;
+}
+
+// 🎯 TEILZIEL 3 (TODO 3): Schreibe ladeBenutzerProfil(userId)
+// Simuliert das asynchrone Laden eines Benutzerprofils:
+// - Wenn userId ein positiver Wert (> 0) ist:
+// Gibt ein Promise zurück, das nach kurzer Zeit (z.B. 10ms) ein Objekt auflöst:
+// { id: userId, name: `Benutzer_${userId}`, status: "aktiv" }
+// - Wenn userId <= 0 oder ungültig:
+// Gibt ein abgewiesenes Promise (reject) mit Error("Ungültige User-ID") zurück.
+function ladeBenutzerProfil(userId) {
+ // TODO: Implementiere ladeBenutzerProfil mit Promise
+ return null;
+}
+
+// 🎯 TEILZIEL 4 (TODO 4): Schreibe ladeAlleDatenQuellen(quelleA, quelleB)
+// Nimmt zwei Promises quelleA und quelleB entgegen und nutzt Promise.all([...]),
+// um beide Datenquellen parallel abzuwarten.
+// Gibt das Promise von Promise.all zurück, das ein Array [ergebnisA, ergebnisB] liefert.
+function ladeAlleDatenQuellen(quelleA, quelleB) {
+ // TODO: Nutze Promise.all([quelleA, quelleB])
+ return null;
+}
diff --git a/courses/javascript/05_callbacks_und_promises/index.html b/courses/javascript/05_callbacks_und_promises/index.html
new file mode 100644
index 0000000..075b145
--- /dev/null
+++ b/courses/javascript/05_callbacks_und_promises/index.html
@@ -0,0 +1,203 @@
+
+
+
+
+
+ JS 05: Callbacks, Event Loop & Promises
+
+
+
+
+
+
+
+
+
+
+
+ ⚡ 1. Synchron vs. Asynchron & Event Loop
+
+ JavaScript ist Single-Threaded . Wenn eine Aufgabe Zeit benötigt (z. B. Timer, Netzwerk-Aufrufe, Dateilesen), darf sie den Hauptthread nicht blockieren.
+
+
+ Der Event Loop sorgt dafür, dass asynchrone Callbacks in einer Warteschlange verwaltet und erst ausgeführt werden, wenn der Haupt-Callstack frei ist.
+
+
+
+
🍔 Die Restaurant-Pieper-Analogie
+
+ Bestellst du in einem Restaurant dein Essen, musst du nicht starr an der Kasse stehen bleiben. Du erhältst sofort einen Bestell-Pieper (ein Promise im Zustand pending) . Du setzt dich entspannt an deinen Tisch (der Event Loop verarbeitet andere Dinge).
+
+
+ Ist dein Menü fertig, vibriert der Pieper (resolve → fulfilled ). Brennt die Küche, blinkt er rot (reject → rejected ).
+
+
+
+
+
+
+ 📊 2. Das Promise-Zustandsdiagramm
+
+ Ein Promise ist ein Stellvertreter-Objekt für einen zukünftigen Wert. Es befindet sich immer in genau einem von drei Zuständen:
+
+
+
+
+ ⏳ pending
+ (wartend / schwebend)
+
+
+
+
→
+
resolve(wert)
+
→
+
+ ✅ fulfilled
+ .then(wert => ...)
+
+
+
+
→
+
reject(error)
+
→
+
+ ❌ rejected
+ .catch(err => ...)
+
+
+
+
+
+
+
+
+ 🛠️ 3. Eigene Promises & Chaining
+ Ein Promise wird mit new Promise((resolve, reject) => { ... }) erzeugt:
+
+ // 1. Promise definieren
+function ladeDaten(id) {
+ return new Promise((resolve, reject) => {
+ if (id > 0) {
+ setTimeout(() => resolve({ id: id, status: "ok" }), 100);
+ } else {
+ reject(new Error("ID muss positiv sein"));
+ }
+ });
+}
+
+// 2. Promise verketten (Chaining)
+ladeDaten(42)
+ .then(daten => {
+ console.log("Geladen:", daten);
+ return daten.id * 2;
+ })
+ .then(verdoppelt => console.log("Verdoppelt:", verdoppelt))
+ .catch(err => console.error("Fehler:", err.message))
+ .finally(() => console.log("Vorgang abgeschlossen."));
+
+ 🚀 Parallelisierung mit Promise.all()
+ // Lädt quelleA und quelleB parallel und wartet auf beide
+Promise.all([quelleA, quelleB])
+ .then(([resA, resB]) => console.log("Beide fertig:", resA, resB))
+ .catch(err => console.error("Mindestens eine Quelle schlug fehl:", err));
+
+
+
+
+
+
+
+
+
diff --git a/courses/javascript/05_callbacks_und_promises/musterloesung.js b/courses/javascript/05_callbacks_und_promises/musterloesung.js
new file mode 100644
index 0000000..91dfc9e
--- /dev/null
+++ b/courses/javascript/05_callbacks_und_promises/musterloesung.js
@@ -0,0 +1,42 @@
+/**
+ * 🌐 JS 05: MUSTERLÖSUNG – CALLBACKS & PROMISES 🌐
+ * ===============================================
+ */
+
+function verzoegereAusfuehrung(ms, rueckgabeWert) {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ resolve(rueckgabeWert);
+ }, ms);
+ });
+}
+
+function pruefeZahlGroesserNull(zahl) {
+ return new Promise((resolve, reject) => {
+ if (typeof zahl === "number" && zahl > 0) {
+ resolve(zahl);
+ } else {
+ reject(new Error("Zahl muss größer als 0 sein"));
+ }
+ });
+}
+
+function ladeBenutzerProfil(userId) {
+ return new Promise((resolve, reject) => {
+ if (typeof userId === "number" && userId > 0) {
+ setTimeout(() => {
+ resolve({
+ id: userId,
+ name: `Benutzer_${userId}`,
+ status: "aktiv"
+ });
+ }, 10);
+ } else {
+ reject(new Error("Ungültige User-ID"));
+ }
+ });
+}
+
+function ladeAlleDatenQuellen(quelleA, quelleB) {
+ return Promise.all([quelleA, quelleB]);
+}
diff --git a/courses/javascript/05_callbacks_und_promises/test_aufgabe.js b/courses/javascript/05_callbacks_und_promises/test_aufgabe.js
new file mode 100644
index 0000000..9c8c3b3
--- /dev/null
+++ b/courses/javascript/05_callbacks_und_promises/test_aufgabe.js
@@ -0,0 +1,78 @@
+// Hilfsfunktion zur Rejection-Prüfung
+async function pruefeReject(promise, expectedMsg, beschreibung) {
+ let threw = false;
+ let caughtErr = null;
+ try {
+ await promise;
+ } catch (err) {
+ threw = true;
+ caughtErr = err;
+ if (expectedMsg && !String(err && (err.message || err)).includes(expectedMsg)) {
+ throw new Error(`${beschreibung}: Erwartete Fehlermeldung "${expectedMsg}", aber erhalten: "${err ? err.message : err}"`);
+ }
+ }
+ if (!threw) {
+ throw new Error(`${beschreibung}: Promise sollte abgewiesen werden (reject), wurde aber erfolgreich aufgelöst.`);
+ }
+}
+
+// 1. Test: verzoegereAusfuehrung
+const erg1 = await verzoegereAusfuehrung(20, "Erfolg 20ms");
+assert.strictEqual(erg1, "Erfolg 20ms", "verzoegereAusfuehrung liefert falschen Wert nach Ablauf des Timers");
+
+// 2. Test: pruefeZahlGroesserNull bei positiver Zahl
+const erg2 = await pruefeZahlGroesserNull(42);
+assert.strictEqual(erg2, 42, "pruefeZahlGroesserNull(42) sollte mit 42 resolven");
+
+// 3. Test: pruefeZahlGroesserNull bei 0 und negativer Zahl
+await pruefeReject(
+ pruefeZahlGroesserNull(-5),
+ "Zahl muss größer als 0 sein",
+ "pruefeZahlGroesserNull(-5)"
+);
+await pruefeReject(
+ pruefeZahlGroesserNull(0),
+ "Zahl muss größer als 0 sein",
+ "pruefeZahlGroesserNull(0)"
+);
+
+// 4. Test: ladeBenutzerProfil mit gültiger ID
+const profil = await ladeBenutzerProfil(101);
+assert.deepStrictEqual(
+ profil,
+ { id: 101, name: "Benutzer_101", status: "aktiv" },
+ "ladeBenutzerProfil(101) liefert falsches Profil-Objekt"
+);
+
+// 5. Test: ladeBenutzerProfil mit ungültiger ID
+await pruefeReject(
+ ladeBenutzerProfil(0),
+ "Ungültige User-ID",
+ "ladeBenutzerProfil(0)"
+);
+await pruefeReject(
+ ladeBenutzerProfil(-10),
+ "Ungültige User-ID",
+ "ladeBenutzerProfil(-10)"
+);
+
+// 6. Test: ladeAlleDatenQuellen mit zwei erfolgreichen Promises (Promise.all)
+const pA = verzoegereAusfuehrung(15, "Sensordaten Alpha");
+const pB = verzoegereAusfuehrung(10, "Sensordaten Beta");
+const alleQuellen = await ladeAlleDatenQuellen(pA, pB);
+assert.deepStrictEqual(
+ alleQuellen,
+ ["Sensordaten Alpha", "Sensordaten Beta"],
+ "ladeAlleDatenQuellen sollte alle Ergebnisse im Array zusammenfassen"
+);
+
+// 7. Test: ladeAlleDatenQuellen mit einem abgewiesenen Promise
+const pGut = verzoegereAusfuehrung(10, "Gute Daten");
+const pSchlecht = pruefeZahlGroesserNull(-100);
+await pruefeReject(
+ ladeAlleDatenQuellen(pGut, pSchlecht),
+ "Zahl muss größer als 0 sein",
+ "ladeAlleDatenQuellen mit abgewiesenem Promise"
+);
+
+console.log("✅ Alle 7 Asynchronitäts- und Promise-Tests in JS 05 erfolgreich bestanden!");
diff --git a/courses/javascript/06_async_await_und_fetch/README.md b/courses/javascript/06_async_await_und_fetch/README.md
new file mode 100644
index 0000000..726ef46
--- /dev/null
+++ b/courses/javascript/06_async_await_und_fetch/README.md
@@ -0,0 +1,142 @@
+# 🌐 JS 06: Async/Await & REST APIs (Fetch)
+
+Willkommen zu **Modul 06** des JavaScript-Kurses! Hier lernst du die modernste und eleganteste Art kennen, um mit asynchronen Daten und Web-APIs in JavaScript zu arbeiten: **`async` und `await`**.
+
+---
+
+## 📖 1. Was ist `async` / `await`?
+
+Mit ECMAScript 2017 (ES8) wurde `async/await` eingeführt. Es ist sogenannte *syntaktische Versüßung* (Syntactic Sugar) über JavaScript Promises:
+- Asynchroner Code sieht plötzlich aus und verhält sich wie **linearer, synchroner Code**.
+- Keine unübersichtlichen `.then()`-Ketten mehr.
+- Fehler werden mit gewohntem `try { ... } catch (err) { ... }` abgefangen.
+
+---
+
+## 📚 2. Die Buch-Lese-Analogie
+
+Stell dir vor, wie du ein Buch liest:
+
+- **Mit Callbacks & Chaining (`.then()`):** Es ist wie ein Buch, bei dem jedes Kapitel auf Seite 80 verweist (*„Wenn du wissen willst, wie es weitergeht, lies Fußnote 4 auf Seite 80 und springe dann zu Kapitel 7“*). Du springst ständig hin und her.
+- **Mit `async/await`:** Du liest die Geschichte ganz natürlich **Zeile für Zeile von oben nach unten**. Wenn an einer Stelle ein Briefbote eintrifft (asynchrones Laden via `await`), pausiert der Erzähler kurz, nimmt den Brief entgegen und liest nahtlos im Text weiter.
+
+---
+
+## ⚙️ 3. Die `async`-Funktion
+
+Wird das Schlüsselwort `async` vor eine Funktion gesetzt:
+1. Gibt die Funktion **immer** ein `Promise` zurück.
+2. Jeder `return`-Wert wird automatisch in ein aufgelöstes Promise (`Promise.resolve(...)`) eingepackt.
+3. Jeder nicht abgefangene Fehler (`throw new Error(...)`) wird zu einem abgewiesenen Promise (`Promise.reject(...)`).
+
+```javascript
+async function holeBegruessung(name) {
+ return `Hallo ${name}!`; // Gibt Promise { : "Hallo Anna!" } zurück
+}
+
+holeBegruessung("Anna").then(text => console.log(text));
+```
+
+---
+
+## ⏳ 4. Der `await`-Operator
+
+`await` kann nur innerhalb von `async`-Funktionen verwendet werden. Es pausiert die Ausführung der aktuellen Funktion, bis das Promise aufgelöst oder abgewiesen wurde:
+
+```javascript
+async function ladeBenutzer() {
+ console.log("Starte Abruf...");
+
+ // Pausiert die Funktion 'ladeBenutzer', aber blockiert NICHT den Browser-Thread!
+ const benutzer = await apiCall("/user/1");
+
+ console.log("Fertig:", benutzer.name);
+ return benutzer;
+}
+```
+
+---
+
+## ⚖️ 5. Direktvergleich: Promises vs. Async/Await
+
+### Vorher: Promise-Chaining (`.then()`)
+```javascript
+function holeDatenMitThen() {
+ return fetch("https://api.beispiel.de/daten")
+ .then(response => {
+ if (!response.ok) throw new Error("Netzwerkfehler");
+ return response.json();
+ })
+ .then(daten => {
+ console.log("Ergebnis:", daten);
+ return daten;
+ })
+ .catch(err => {
+ console.error("Fehler:", err.message);
+ });
+}
+```
+
+### Jetzt: Modernes `async/await`
+```javascript
+async function holeDatenMitAsync() {
+ try {
+ const response = await fetch("https://api.beispiel.de/daten");
+ if (!response.ok) throw new Error(`HTTP-Fehler: ${response.status}`);
+
+ const daten = await response.json();
+ console.log("Ergebnis:", daten);
+ return daten;
+ } catch (err) {
+ console.error("Fehler:", err.message);
+ }
+}
+```
+
+---
+
+## 🌐 6. REST-APIs abfragen mit `fetch()`
+
+Beim Abfragen einer REST-API mit `fetch()` laufen in der Praxis zwei asynchrone Schritte ab:
+
+1. **HTTP-Verbindung aufbauen & Statuscode prüfen:**
+ ```javascript
+ const response = await fetch(url);
+ if (!response.ok) {
+ // response.status enthält den HTTP-Code (z. B. 404 Not Found oder 500 Server Error)
+ throw new Error(`Fehler: ${response.status}`);
+ }
+ ```
+2. **JSON-Body asynchron parsen:**
+ ```javascript
+ const jsonData = await response.json();
+ ```
+
+---
+
+## 🛡️ 7. Robuste Fehlerbehandlung mit `try ... catch`
+
+Mit `try ... catch` können sowohl synchrone JavaScript-Laufzeitfehler als auch asynchrone Promise-Rejections an zentraler Stelle abgefangen werden:
+
+```javascript
+async function sichereAktion() {
+ try {
+ const erg = await riskanteOperation();
+ return erg;
+ } catch (err) {
+ console.warn("Operation fehlgeschlagen, verwende Fallback:", err.message);
+ return { status: "fallback_aktiv" };
+ }
+}
+```
+
+---
+
+## 🎯 8. Deine Aufgaben in `aufgabe.js`
+
+1. **`holeWetterDaten(stadtName, mockApiFetcher)`**:
+ Rufe `await mockApiFetcher(stadtName)` auf. Prüfe `response.ok`. Wenn `false`, wirf einen Fehler. Wenn `true`, parse JSON (`await response.json()`) und gib `{ stadt, temperatur, wetterlage }` zurück.
+2. **`aggregiereBenutzerPosts(userId, mockPostApiFetcher)`**:
+ Rufe `await mockPostApiFetcher(userId)` auf. Aggregiere die Posts zu `{ userId, anzahlPosts, postTitel }`.
+3. **`sichererApiAufruf(apiPromiseOderFn, standardFallback)`**:
+ Führe ein Promise oder eine async Funktion im `try`-Block aus. Fange Fehler im `catch`-Block ab und gib `standardFallback` zurück.
diff --git a/courses/javascript/06_async_await_und_fetch/aufgabe.js b/courses/javascript/06_async_await_und_fetch/aufgabe.js
new file mode 100644
index 0000000..0a85b75
--- /dev/null
+++ b/courses/javascript/06_async_await_und_fetch/aufgabe.js
@@ -0,0 +1,42 @@
+/**
+ * 🌐 JS 06: ASYNC / AWAIT & REST-APIS (FETCH) 🌐
+ * ===============================================
+ * Lerne modernes asynchrones JavaScript mit async/await,
+ * robuster try-catch-Fehlerbehandlung und dem Konsumieren von REST-APIs.
+ */
+
+// 🎯 TEILZIEL 1 (TODO 1): Schreibe holeWetterDaten(stadtName, mockApiFetcher)
+// - Ruft mockApiFetcher(stadtName) mit await auf.
+// - Das Response-Objekt enthält { ok: boolean, status: number, json: async () => data }.
+// - Wenn response.ok === false ist: wirf einen Fehler (throw new Error(`Stadt ${stadtName} nicht gefunden`)).
+// - Wenn response.ok === true ist: parse das JSON mit await response.json()
+// - Gib ein formatiertes Objekt zurück:
+// { stadt: data.stadt, temperatur: data.temperatur, wetterlage: data.wetterlage }
+async function holeWetterDaten(stadtName, mockApiFetcher) {
+ // TODO: Nutze await, prüfe response.ok, parse JSON und gib das formatierte Objekt zurück
+ return null;
+}
+
+// 🎯 TEILZIEL 2 (TODO 2): Schreibe aggregiereBenutzerPosts(userId, mockPostApiFetcher)
+// - Ruft mockPostApiFetcher(userId) mit await auf.
+// - Wenn der Fetcher eine Response liefert (response.ok), parse das JSON; wenn direkt ein Array geliefert wird, nutze dieses.
+// - Extrahiere alle Posts des Nutzers.
+// - Gib ein Aggregat-Objekt zurück mit folgendem Aufbau:
+// {
+// userId: userId,
+// anzahlPosts: posts.length,
+// postTitel: posts.map(p => p.titel)
+// }
+async function aggregiereBenutzerPosts(userId, mockPostApiFetcher) {
+ // TODO: Asynchron abrufen und aggregieren
+ return null;
+}
+
+// 🎯 TEILZIEL 3 (TODO 3): Schreibe sichererApiAufruf(apiPromiseOderFn, standardFallback)
+// - Führt das übergebene Promise oder die asynchrone Funktion in einem try-Block mit await aus.
+// - Wenn die Ausführung erfolgreich ist: gib das Ergebnis zurück.
+// - Wenn ein Fehler auftritt (catch): fange den Fehler ab und gib standardFallback zurück.
+async function sichererApiAufruf(apiPromiseOderFn, standardFallback) {
+ // TODO: try-catch Block für sicheren Aufruf mit Fallback-Wert
+ return null;
+}
diff --git a/courses/javascript/06_async_await_und_fetch/index.html b/courses/javascript/06_async_await_und_fetch/index.html
new file mode 100644
index 0000000..e1fc739
--- /dev/null
+++ b/courses/javascript/06_async_await_und_fetch/index.html
@@ -0,0 +1,189 @@
+
+
+
+
+
+ JS 06: Async/Await & REST-APIs (Fetch)
+
+
+
+
+
+
+
+
+
+
+
+ 📖 1. Warum Async/Await?
+
+ Mit async/await (ES2017) gehört unübersichtliches Promise-Chaining der Vergangenheit an. Asynchroner Code liest sich nun von oben nach unten wie ganz normaler, synchroner Code.
+
+
+
+
📚 Die Buch-Lese-Analogie
+
+ Vorher (Callbacks & .then()): Wie ein Buch, bei dem jedes Kapitel auf Fußnoten verweist („Lies auf Seite 80 weiter, um die Antwort zu finden“ ).
+
+
+ Jetzt (Async/Await): Du liest das Buch ganz entspannt Zeile für Zeile von oben nach unten . Bei await pausiert der Erzähler kurz, holt die Information ab und liest genau an der Stelle flüssig weiter!
+
+
+
+
+
+
+ ⚖️ 2. Gegenüberstellung: Promises vs. Async/Await
+
+
+
⏳ Früher: Promise .then() Chaining
+
function ladeProfil(id) {
+ fetch(`/api/user/${id}`)
+ .then(res => {
+ if (!res.ok) throw new Error("404");
+ return res.json();
+ })
+ .then(user => {
+ console.log("Name:", user.name);
+ })
+ .catch(err => {
+ console.error("Fehler:", err);
+ });
+}
+
+
+
+
✨ Heute: Modernes Async/Await
+
async function ladeProfil(id) {
+ try {
+ const res = await fetch(`/api/user/${id}`);
+ if (!res.ok) throw new Error("404");
+
+ const user = await res.json();
+ console.log("Name:", user.name);
+ } catch (err) {
+ console.error("Fehler:", err);
+ }
+}
+
+
+
+
+
+
+ 🌐 3. REST-APIs konsumieren
+ Typischer 2-Schritt-Ablauf beim Abrufen einer JSON-REST-API:
+
+
+
Schritt 1
+
+
HTTP-Request senden & Statuscode prüfen:
+
+ const res = await fetch(url);
+ if (!res.ok) throw new Error(`HTTP-Fehler: ${res.status}`);
+
+
+
+
+
+
Schritt 2
+
+
JSON-Body asynchron auslesen:
+
+ const daten = await res.json();
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/courses/javascript/06_async_await_und_fetch/musterloesung.js b/courses/javascript/06_async_await_und_fetch/musterloesung.js
new file mode 100644
index 0000000..afd0028
--- /dev/null
+++ b/courses/javascript/06_async_await_und_fetch/musterloesung.js
@@ -0,0 +1,50 @@
+/**
+ * 🌐 JS 06: MUSTERLÖSUNG – ASYNC / AWAIT & REST-APIS 🌐
+ * ====================================================
+ */
+
+async function holeWetterDaten(stadtName, mockApiFetcher) {
+ const response = await mockApiFetcher(stadtName);
+ if (!response || !response.ok) {
+ const status = response ? response.status : 500;
+ throw new Error(`Stadt ${stadtName} nicht gefunden (Status: ${status})`);
+ }
+ const data = await response.json();
+ return {
+ stadt: data.stadt,
+ temperatur: data.temperatur,
+ wetterlage: data.wetterlage
+ };
+}
+
+async function aggregiereBenutzerPosts(userId, mockPostApiFetcher) {
+ const response = await mockPostApiFetcher(userId);
+ let posts;
+ if (response && typeof response.json === "function") {
+ if (!response.ok) {
+ throw new Error(`Fehler beim Laden der Posts für User ${userId}`);
+ }
+ posts = await response.json();
+ } else if (Array.isArray(response)) {
+ posts = response;
+ } else {
+ posts = [];
+ }
+
+ return {
+ userId: userId,
+ anzahlPosts: posts.length,
+ postTitel: posts.map((p) => p.titel)
+ };
+}
+
+async function sichererApiAufruf(apiPromiseOderFn, standardFallback) {
+ try {
+ if (typeof apiPromiseOderFn === "function") {
+ return await apiPromiseOderFn();
+ }
+ return await apiPromiseOderFn;
+ } catch (err) {
+ return standardFallback;
+ }
+}
diff --git a/courses/javascript/06_async_await_und_fetch/test_aufgabe.js b/courses/javascript/06_async_await_und_fetch/test_aufgabe.js
new file mode 100644
index 0000000..593844b
--- /dev/null
+++ b/courses/javascript/06_async_await_und_fetch/test_aufgabe.js
@@ -0,0 +1,138 @@
+// Hilfsfunktion für Fehler-Prüfung
+async function pruefeReject(promise, expectedMsg, beschreibung) {
+ let threw = false;
+ let caughtErr = null;
+ try {
+ await promise;
+ } catch (err) {
+ threw = true;
+ caughtErr = err;
+ if (expectedMsg && !String(err && (err.message || err)).includes(expectedMsg)) {
+ throw new Error(`${beschreibung}: Erwartete Fehlermeldung "${expectedMsg}", aber erhalten: "${err ? err.message : err}"`);
+ }
+ }
+ if (!threw) {
+ throw new Error(`${beschreibung}: Funktion sollte Fehler werfen / rejecten, war aber erfolgreich.`);
+ }
+}
+
+// ----------------------------------------------------
+// Mock-APIs definieren
+// ----------------------------------------------------
+const mockWetterApi = async (stadt) => {
+ if (stadt === "Berlin") {
+ return {
+ ok: true,
+ status: 200,
+ json: async () => ({ stadt: "Berlin", temperatur: 21, wetterlage: "Sonnig", feuchtigkeit: "55%" })
+ };
+ } else if (stadt === "München") {
+ return {
+ ok: true,
+ status: 200,
+ json: async () => ({ stadt: "München", temperatur: 18, wetterlage: "Regnerisch" })
+ };
+ } else {
+ return {
+ ok: false,
+ status: 404,
+ json: async () => ({ fehler: "Stadt nicht gefunden" })
+ };
+ }
+};
+
+const mockPostApi = async (userId) => {
+ if (userId === 10) {
+ return {
+ ok: true,
+ status: 200,
+ json: async () => [
+ { id: 1, titel: "Einführung in JS" },
+ { id: 2, titel: "Async Await Guide" },
+ { id: 3, titel: "REST-APIs verstehen" }
+ ]
+ };
+ } else if (userId === 99) {
+ return [
+ { id: 4, titel: "Einzelbeitrag" }
+ ];
+ } else {
+ return {
+ ok: true,
+ status: 200,
+ json: async () => []
+ };
+ }
+};
+
+// ----------------------------------------------------
+// 1. Test: holeWetterDaten (Erfolg)
+// ----------------------------------------------------
+const wetterBerlin = await holeWetterDaten("Berlin", mockWetterApi);
+assert.deepStrictEqual(
+ wetterBerlin,
+ { stadt: "Berlin", temperatur: 21, wetterlage: "Sonnig" },
+ "holeWetterDaten('Berlin') liefert unerwartetes Wetter-Objekt"
+);
+
+const wetterMuenchen = await holeWetterDaten("München", mockWetterApi);
+assert.strictEqual(wetterMuenchen.temperatur, 18, "Temperatur für München sollte 18 sein");
+assert.strictEqual(wetterMuenchen.wetterlage, "Regnerisch", "Wetterlage für München sollte Regnerisch sein");
+
+// ----------------------------------------------------
+// 2. Test: holeWetterDaten (404 Fehler)
+// ----------------------------------------------------
+await pruefeReject(
+ holeWetterDaten("Atlantis", mockWetterApi),
+ "Atlantis",
+ "holeWetterDaten für unbekannte Stadt muss Fehler werfen"
+);
+
+// ----------------------------------------------------
+// 3. Test: aggregiereBenutzerPosts
+// ----------------------------------------------------
+const posts10 = await aggregiereBenutzerPosts(10, mockPostApi);
+assert.deepStrictEqual(
+ posts10,
+ {
+ userId: 10,
+ anzahlPosts: 3,
+ postTitel: ["Einführung in JS", "Async Await Guide", "REST-APIs verstehen"]
+ },
+ "aggregiereBenutzerPosts(10) liefert falsches Aggregat"
+);
+
+const posts99 = await aggregiereBenutzerPosts(99, mockPostApi);
+assert.strictEqual(posts99.anzahlPosts, 1, "User 99 sollte genau 1 Post haben");
+assert.deepStrictEqual(posts99.postTitel, ["Einzelbeitrag"], "Post-Titel für User 99 fehlerhaft");
+
+const postsLeer = await aggregiereBenutzerPosts(999, mockPostApi);
+assert.strictEqual(postsLeer.anzahlPosts, 0, "User 999 sollte 0 Posts haben");
+assert.deepStrictEqual(postsLeer.postTitel, [], "Leere Post-Titel-Liste erwartet");
+
+// ----------------------------------------------------
+// 4. Test: sichererApiAufruf
+// ----------------------------------------------------
+// 4a: Erfolgreiches Promise
+const ergErfolg = await sichererApiAufruf(Promise.resolve("Daten geladen!"), "Standard");
+assert.strictEqual(ergErfolg, "Daten geladen!", "sichererApiAufruf sollte den aufgelösten Wert zurückgeben");
+
+// 4b: Fehlgeschlagenes Promise (muss abgefangen werden)
+const ergFehler = await sichererApiAufruf(
+ Promise.reject(new Error("Netzwerk-Timeout")),
+ "Standard-Fallback"
+);
+assert.strictEqual(ergFehler, "Standard-Fallback", "sichererApiAufruf sollte Fallback zurückgeben bei Rejection");
+
+// 4c: Async Funktion, die eine Exception wirft
+const ergFnFehler = await sichererApiAufruf(
+ async () => { throw new Error("Kritischer API Crash"); },
+ { status: "offline", data: [] }
+);
+assert.deepStrictEqual(
+ ergFnFehler,
+ { status: "offline", data: [] },
+ "sichererApiAufruf sollte Fallback-Objekt bei geworfenem Fehler liefern"
+);
+
+console.log("✅ Alle Tests in JS 06 (Async/Await & REST-APIs) erfolgreich bestanden!");
diff --git a/courses/javascript/07_einstieg_typescript/README.md b/courses/javascript/07_einstieg_typescript/README.md
new file mode 100644
index 0000000..c676f0f
--- /dev/null
+++ b/courses/javascript/07_einstieg_typescript/README.md
@@ -0,0 +1,151 @@
+# 🛡️ Modul 07: TypeScript Type Safety & Interfaces
+
+> **Leitgedanke:** *„TypeScript ist wie ein digitaler Sicherheitsgurt und ein unbestechlicher Bauplan-Wächter: Fehler werden bereits während der Entwicklung im Editor erkannt – und nicht erst nachts beim Kunden auf der Website!“*
+
+---
+
+## 📚 Übersicht & Lernziele
+
+In diesem Modul machst du den entscheidenden Schritt von dynamischem JavaScript hin zur **statischen Typsicherheit mit TypeScript**:
+
+1. **Warum TypeScript?** – Verstehe den Unterschied zwischen Laufzeit- und Compile-Time-Fehlern.
+2. **Basis-Datentypen** – `string`, `number`, `boolean`, `string[]`, `any`, `unknown`, `void`.
+3. **Type Aliases & Union Types** – Flexible Kombinationen wie `type ID = string | number;`.
+4. **Interfaces & Optionale Eigenschaften** – Strukturverträge mit `interface` und `?`.
+5. **Generics (``)** – Wiederverwendbare, typsichere Algorithmen und Hilfsfunktionen.
+6. **Optional Chaining (`?.`) & Nullish Coalescing (`??`)** – Sichere Zugriffe ohne `Cannot read properties of undefined`.
+
+---
+
+## 💡 1. Die didaktische Analogie: Der Bauplan-Wächter
+
+| Klassisches JavaScript ⚡ | TypeScript mit Interfaces 🛡️ |
+| :--- | :--- |
+| **Baustelle ohne Plan:** Man baut drauflos. Ob ein Stein passt oder eine tragende Wand fehlt, merkt man erst, wenn das Haus einstürzt (Laufzeit-Crash beim Kunden). | **Architekten-Bauplan mit TÜV-Prüfung:** Jede Schnittstelle, jeder Balken und jedes Kabel ist exakt typisiert. Passt ein Element nicht, schlägt der Compiler sofort Alarm. |
+
+---
+
+## 🔍 2. TypeScript Kernkonzepte im Detail
+
+### A. Basis-Datentypen
+
+```typescript
+// Explizite Typ-Annotationen
+const benutzername: string = "Anna_Dev";
+const alter: number = 28;
+const istAdmin: boolean = true;
+
+// Arrays
+const tags: string[] = ["typescript", "frontend", "web"];
+const noten: Array = [1, 2, 1, 3];
+
+// Spezialtypen: any vs. unknown vs. void
+let beliebig: any = "Kann alles sein (Typ-Check deaktiviert)";
+let unsicher: unknown = JSON.parse('{"status": "ok"}'); // Erfordert Typprüfung vor Nutzung!
+
+function loggeNachricht(msg: string): void {
+ console.log(`[LOG]: ${msg}`);
+ // Kein Return-Wert -> void
+}
+```
+
+### B. Type Aliases & Union Types
+
+Mit Union Types (`|`) können Variablen mehrere definierte Typen annehmen:
+
+```typescript
+// Type Alias für eindeutige Identifikatoren
+type ID = string | number;
+
+// Literal-Union-Type für feste Status-Werte
+type AuftragsStatus = "offen" | "in_bearbeitung" | "abgeschlossen" | "storniert";
+
+let meineId: ID = 42;
+meineId = "USR-9921"; // ✅ Beides absolut gültig!
+```
+
+### C. Interfaces (Objekt-Verträge)
+
+Ein Interface definiert die exakte Form eines JavaScript-Objekts. Mit dem Fragezeichen `?` werden Felder als optional markiert:
+
+```typescript
+interface User {
+ id: ID;
+ name: string;
+ istAktiv: boolean;
+ email?: string; // ❓ Optional: Kann vorhanden sein oder undefined
+ erstelltAm: string;
+}
+
+const kunde1: User = {
+ id: 101,
+ name: "Max Mustermann",
+ istAktiv: true,
+ erstelltAm: "2026-08-30T00:00:00.000Z"
+};
+```
+
+### D. Generics (`` – Typ-Parameter)
+
+Generics erlauben es, Funktionen und Klassen zu schreiben, die mit beliebigen Datentypen arbeiten, dabei aber die volle Typsicherheit behalten:
+
+```typescript
+// Generische Umkehrfunktion für Arrays
+function umkehren(liste: T[]): T[] {
+ return [...liste].reverse();
+}
+
+const zahlenUmgekehrt = umkehren([1, 2, 3, 4]); // Typ: number[]
+const worteUmgekehrt = umkehren(["A", "B", "C"]); // Typ: string[]
+```
+
+### E. Optional Chaining (`?.`) & Nullish Coalescing (`??`)
+
+```typescript
+interface Profil {
+ avatar?: {
+ url?: string;
+ };
+}
+
+const user: Profil = {};
+
+// Sicherer Zugriff ohne Absturz:
+const avatarUrl = user?.avatar?.url ?? "default_avatar.png";
+console.log(avatarUrl); // -> "default_avatar.png"
+```
+
+---
+
+## 🎯 3. Deine Aufgaben in `aufgabe.js`
+
+Implementiere die folgenden 4 Kernfunktionen:
+
+### 🎯 TODO 1: `erstelleKunde(id, name, istAktiv = true, email = null)`
+- Erzeugt ein valides Kunden-Objekt `{ id, name, istAktiv, [email], erstelltAm }`.
+- Wirft einen `TypeError`, wenn `id` weder `string` noch `number` ist, oder wenn `name` leer ist.
+- Säubert den Namen mit `.trim()`.
+
+### 🎯 TODO 2: `validiereAuftrag(auftragObjekt)`
+- Validiert ein übergebenes Auftrag-Objekt gegen das `Auftrag`-Interface.
+- Prüft `id`, `kundeId`, `positionen` (Array mit mind. 1 Element, validen Mengen > 0 & Einzelpreisen >= 0), `summe` (>= 0) und `status` ('offen', 'in_bearbeitung', 'abgeschlossen', 'storniert').
+- Liefert `{ gueltig: boolean, fehler: string[] }`.
+
+### 🎯 TODO 3: `filtereNachStatus(eintraege, zielStatus)`
+- Generische Hilfsfunktion: Filtert ein Array von Objekten nach `item.status === zielStatus`.
+- Liefert bei ungültigen Eingaben sicher ein leeres Array `[]`.
+
+### 🎯 TODO 4: `formatierePreisangabe(betrag, waehrung = "EUR")`
+- Formatiert Zahlen typsicher im deutschen Währungsformat (z.B. `49.99` -> `"49,99 EUR"`).
+- Wirft bei ungültigen Beträgen (z.B. Strings, `NaN`) oder leeren Währungen einen `TypeError`.
+
+---
+
+## 🧪 4. Selbstüberprüfung
+
+Führe die Unittests in der Web-IDE oder im Test-Runner aus:
+
+- ✅ 1. Kunden-Erstellung mit Standardwerten & Fehlervalidierung.
+- ✅ 2. Vollständige Auftragsvalidierung mit Fehler-Reporting.
+- ✅ 3. Generischer Statusfilter.
+- ✅ 4. Typsichere Preis- und Währungsformatierung.
diff --git a/courses/javascript/07_einstieg_typescript/aufgabe.js b/courses/javascript/07_einstieg_typescript/aufgabe.js
new file mode 100644
index 0000000..7a27e8f
--- /dev/null
+++ b/courses/javascript/07_einstieg_typescript/aufgabe.js
@@ -0,0 +1,122 @@
+/**
+ * 🌐 JS/TS 07: TYPESCRIPT TYPE SAFETY & INTERFACES 🌐
+ * ====================================================
+ * In diesem Modul lernst du, wie statische Typsicherheit,
+ * Interfaces, Type Aliases und Generics deine Software vor
+ * Laufzeitfehlern schützen ("Digitaler Sicherheitsgurt").
+ */
+
+/**
+ * @typedef {Object} Kunde
+ * @property {string | number} id - Eindeutige Kunden-ID
+ * @property {string} name - Vollständiger Name des Kunden
+ * @property {boolean} istAktiv - Status des Kunden
+ * @property {string} [email] - Optionale E-Mail-Adresse
+ * @property {string} erstelltAm - ISO-Datumsstring der Erstellung
+ */
+
+/**
+ * 🎯 TEILZIEL 1 (TODO 1): erstelleKunde(id, name, istAktiv = true, email = null)
+ * Erstellt ein typsicheres Kundenobjekt.
+ *
+ * Anforderungen:
+ * - id: string | number (nicht leer)
+ * - name: string (darf nach .trim() nicht leer sein)
+ * - istAktiv: boolean (Standard: true)
+ * - email: string | null (optional)
+ * - erstelltAm: aktueller ISO-Datumsstring (z.B. new Date().toISOString())
+ * - Wirft einen TypeError bei ungültigen Typen oder leerem Namen!
+ *
+ * @param {string | number} id
+ * @param {string} name
+ * @param {boolean} [istAktiv=true]
+ * @param {string} [email=null]
+ * @returns {Kunde}
+ */
+function erstelleKunde(id, name, istAktiv = true, email = null) {
+ // TODO: Validierung & Erstellung des Kunden-Objekts implementieren
+ return null;
+}
+
+/**
+ * @typedef {Object} Auftragsposition
+ * @property {string} titel
+ * @property {number} menge
+ * @property {number} einzelpreis
+ */
+
+/**
+ * @typedef {'offen' | 'in_bearbeitung' | 'abgeschlossen' | 'storniert'} AuftragsStatus
+ */
+
+/**
+ * @typedef {Object} Auftrag
+ * @property {string | number} id
+ * @property {string | number} kundeId
+ * @property {Auftragsposition[]} positionen
+ * @property {number} summe
+ * @property {AuftragsStatus} status
+ */
+
+/**
+ * @typedef {Object} ValidierungsErgebnis
+ * @property {boolean} gueltig
+ * @property {string[]} fehler
+ */
+
+/**
+ * 🎯 TEILZIEL 2 (TODO 2): validiereAuftrag(auftragObjekt)
+ * Prüft ein Auftrag-Objekt auf vollständige Einhaltung aller Interface-Regeln:
+ * - Objekt vorhanden (nicht null/undefined)
+ * - id & kundeId vorhanden (string oder number)
+ * - positionen ist ein Array mit mind. 1 Element. Jedes Element hat:
+ * - titel (string, nicht leer)
+ * - menge (number > 0)
+ * - einzelpreis (number >= 0)
+ * - summe ist eine Zahl >= 0
+ * - status ist einer der erlaubten Werte: 'offen' | 'in_bearbeitung' | 'abgeschlossen' | 'storniert'
+ *
+ * @param {any} auftragObjekt
+ * @returns {ValidierungsErgebnis} { gueltig: boolean, fehler: string[] }
+ */
+function validiereAuftrag(auftragObjekt) {
+ // TODO: Typprüfung und Validierungsregeln implementieren
+ return { gueltig: false, fehler: ["Noch nicht implementiert"] };
+}
+
+/**
+ * 🎯 TEILZIEL 3 (TODO 3): filtereNachStatus(eintraege, zielStatus)
+ * Generische Filterfunktion: Filtert eine Liste beliebiger Objekte nach ihrem status-Feld.
+ *
+ * Anforderungen:
+ * - Wenn eintraege kein Array ist, leeres Array [] zurückgeben.
+ * - Gibt alle Elemente zurück, deren item.status exakt mit zielStatus übereinstimmt.
+ *
+ * @template T
+ * @param {T[]} eintraege
+ * @param {string} zielStatus
+ * @returns {T[]}
+ */
+function filtereNachStatus(eintraege, zielStatus) {
+ // TODO: Generische Filterung implementieren
+ return [];
+}
+
+/**
+ * 🎯 TEILZIEL 4 (TODO 4): formatierePreisangabe(betrag, waehrung = "EUR")
+ * Formatiert einen numerischen Geldbetrag mit 2 Nachkommastellen und Währungssymbol.
+ *
+ * Anforderungen:
+ * - betrag muss eine gültige Zahl sein (typeof === 'number' und !isNaN)
+ * - waehrung muss ein nicht-leerer String sein (Standard: "EUR")
+ * - Deutsches Format mit Komma als Dezimaltrenner: z.B. 49.99 -> "49,99 EUR"
+ * - Wirft TypeError bei ungültigem Betrag oder ungültiger Währung.
+ *
+ * @param {number} betrag
+ * @param {string} [waehrung="EUR"]
+ * @returns {string} z.B. "49,99 EUR"
+ */
+function formatierePreisangabe(betrag, waehrung = "EUR") {
+ // TODO: Formatierung und Typprüfung implementieren
+ return "";
+}
diff --git a/courses/javascript/07_einstieg_typescript/index.html b/courses/javascript/07_einstieg_typescript/index.html
new file mode 100644
index 0000000..08af596
--- /dev/null
+++ b/courses/javascript/07_einstieg_typescript/index.html
@@ -0,0 +1,175 @@
+
+
+
+
+
+ TS 07: TypeScript Type Safety & Interfaces
+
+
+
+
+
+
+
+
+
+
+
+ 📂 Dateien dieser Einheit:
+
+
+
+
+
+
+ 1. 🛡️ Warum TypeScript? Der digitale Sicherheitsgurt
+
+ In reinem JavaScript treten Typfehler oft erst spät zur Laufzeit auf – beispielsweise wenn ein Kunde im Online-Shop auf „Kaufen“ klickt und das Programm mit TypeError: Cannot read properties of undefined abstürzt.
+
+
+ TypeScript erweitert JavaScript um statische Typüberprüfungen. Es wirkt wie ein digitaler Sicherheitsgurt und ein unbestechlicher Bauplan-Wächter : Jeder Parameter, jedes Objekt und jede Schnittstelle wird genau geprüft, noch bevor der Code im Browser ausgeführt wird.
+
+
+
+
🏗️ Die Architekten-Analogie:
+ JavaScript ohne Typen ist wie eine Baustelle ohne Plan: Man merkt erst beim Einzug, dass das Treppenhaus fehlt. TypeScript liefert den geprüften Bauplan: Passt ein Baustein nicht, warnt dich der Compiler sofort im Editor!
+
+
+
+
+
+ 2. 📋 TypeScript Cheat-Sheet: Basis-Typen & Union Types
+ TypeScript bietet alle vertrauten Datentypen sowie mächtige Kompositionswerkzeuge:
+
+
+
+
+const name : string = "Max Mustermann" ;
+const alter : number = 34 ;
+const aktiv : boolean = true ;
+const hobbies : string[] = ["Coding" , "Design" ];
+
+
+type ID = string | number ;
+type Status = "offen" | "in_bearbeitung" | "abgeschlossen" | "storniert" ;
+
+
+let beliebig : any = "Gefährlich: Deaktiviert alle Checks" ;
+let sicherer : unknown = JSON.parse (daten);
+function loggen (msg : string ): void { console.log (msg ); }
+
+
+
+
+
+ 3. 📐 Interfaces: Verträge für Datenstrukturen
+ Ein interface beschreibt die exakte Form von Objekten. Optionale Eigenschaften werden mit ? gekennzeichnet:
+
+
+
📊 Interface-Diagramm: Auftrag & Positionen
+
+ interface Auftrag {
+ id: string | number ;
+ kundeId: string | number ;
+ positionen: Auftragsposition[] ;
+ summe: number ;
+ status: AuftragsStatus ;
+ }
+
+
+ interface Auftragsposition {
+ titel: string ;
+ menge: number ;
+ einzelpreis: number ;
+ }
+
+
+
+
+ 💡 Optional Chaining & Nullish Coalescing:
+ Nutze kunde?.adresse?.stadt ?? 'Unbekannt', um verschachtelte Eigenschaften sicher abzurufen, ohne dass Fehler bei null oder undefined auftreten.
+
+
+
+
+
+ 4. 🧬 Generics: Typ-Parameter für flexible Funktionen
+
+ Mit Generics (<T>) schreibst du Funktionen, die für beliebige Typen funktionieren, ohne den Typenverlust von any:
+
+ function umkehren<T>(liste: T[]): T[] {
+ return [...liste].reverse();
+}
+
+const zahlen = umkehren([1, 2, 3]); // Typ bleibt number[]
+const strings = umkehren(["A", "B", "C"]); // Typ bleibt string[]
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/courses/javascript/07_einstieg_typescript/musterloesung.js b/courses/javascript/07_einstieg_typescript/musterloesung.js
new file mode 100644
index 0000000..f0253be
--- /dev/null
+++ b/courses/javascript/07_einstieg_typescript/musterloesung.js
@@ -0,0 +1,132 @@
+/**
+ * 🌐 JS/TS 07: TYPESCRIPT TYPE SAFETY & INTERFACES (MUSTERLÖSUNG) 🌐
+ * ==================================================================
+ */
+
+/**
+ * @typedef {Object} Kunde
+ * @property {string | number} id - Eindeutige Kunden-ID
+ * @property {string} name - Vollständiger Name des Kunden
+ * @property {boolean} istAktiv - Status des Kunden
+ * @property {string} [email] - Optionale E-Mail-Adresse
+ * @property {string} erstelltAm - ISO-Datumsstring der Erstellung
+ */
+
+/**
+ * 🎯 TEILZIEL 1: erstelleKunde(id, name, istAktiv = true, email = null)
+ */
+function erstelleKunde(id, name, istAktiv = true, email = null) {
+ if (id === null || id === undefined || id === "" || (typeof id !== "string" && typeof id !== "number")) {
+ throw new TypeError("Ungültige Kunden-ID: string oder number erwartet");
+ }
+
+ if (typeof name !== "string" || name.trim().length === 0) {
+ throw new TypeError("Ungültiger Kunden-Name: nicht-leerer String erwartet");
+ }
+
+ if (typeof istAktiv !== "boolean") {
+ istAktiv = Boolean(istAktiv);
+ }
+
+ if (email !== null && email !== undefined && (typeof email !== "string" || email.trim() === "")) {
+ throw new TypeError("Ungültige E-Mail-Adresse");
+ }
+
+ const kunde = {
+ id: id,
+ name: name.trim(),
+ istAktiv: istAktiv,
+ erstelltAm: new Date().toISOString()
+ };
+
+ if (email && typeof email === "string" && email.trim().length > 0) {
+ kunde.email = email.trim();
+ }
+
+ return kunde;
+}
+
+/**
+ * 🎯 TEILZIEL 2: validiereAuftrag(auftragObjekt)
+ */
+function validiereAuftrag(auftragObjekt) {
+ if (!auftragObjekt || typeof auftragObjekt !== "object" || Array.isArray(auftragObjekt)) {
+ return { gueltig: false, fehler: ["Auftrag muss ein gültiges Objekt sein"] };
+ }
+
+ const fehler = [];
+
+ // ID Validierung
+ if (auftragObjekt.id === undefined || auftragObjekt.id === null || auftragObjekt.id === "" ||
+ (typeof auftragObjekt.id !== "string" && typeof auftragObjekt.id !== "number")) {
+ fehler.push("Ungültige Auftrags-ID");
+ }
+
+ // Kunde-ID Validierung
+ if (auftragObjekt.kundeId === undefined || auftragObjekt.kundeId === null || auftragObjekt.kundeId === "" ||
+ (typeof auftragObjekt.kundeId !== "string" && typeof auftragObjekt.kundeId !== "number")) {
+ fehler.push("Ungültige Kunden-ID");
+ }
+
+ // Positionen Validierung
+ if (!Array.isArray(auftragObjekt.positionen) || auftragObjekt.positionen.length === 0) {
+ fehler.push("Mindestens eine Position erforderlich");
+ } else {
+ auftragObjekt.positionen.forEach((pos, idx) => {
+ if (!pos || typeof pos !== "object") {
+ fehler.push(`Position ${idx + 1} ist kein gültiges Objekt`);
+ } else {
+ if (typeof pos.titel !== "string" || pos.titel.trim() === "") {
+ fehler.push(`Position ${idx + 1}: Titel fehlt oder ist leer`);
+ }
+ if (typeof pos.menge !== "number" || isNaN(pos.menge) || pos.menge <= 0) {
+ fehler.push(`Position ${idx + 1}: Menge muss > 0 sein`);
+ }
+ if (typeof pos.einzelpreis !== "number" || isNaN(pos.einzelpreis) || pos.einzelpreis < 0) {
+ fehler.push(`Position ${idx + 1}: Einzelpreis muss >= 0 sein`);
+ }
+ }
+ });
+ }
+
+ // Summen-Validierung
+ if (typeof auftragObjekt.summe !== "number" || isNaN(auftragObjekt.summe) || auftragObjekt.summe < 0) {
+ fehler.push("Summe muss eine Zahl >= 0 sein");
+ }
+
+ // Status Validierung
+ const erlaubteStatus = ["offen", "in_bearbeitung", "abgeschlossen", "storniert"];
+ if (!erlaubteStatus.includes(auftragObjekt.status)) {
+ fehler.push("Status ungültig (erlaubt: offen, in_bearbeitung, abgeschlossen, storniert)");
+ }
+
+ return {
+ gueltig: fehler.length === 0,
+ fehler: fehler
+ };
+}
+
+/**
+ * 🎯 TEILZIEL 3: filtereNachStatus(eintraege, zielStatus)
+ */
+function filtereNachStatus(eintraege, zielStatus) {
+ if (!Array.isArray(eintraege)) {
+ return [];
+ }
+ return eintraege.filter(item => item && typeof item === "object" && item.status === zielStatus);
+}
+
+/**
+ * 🎯 TEILZIEL 4: formatierePreisangabe(betrag, waehrung = "EUR")
+ */
+function formatierePreisangabe(betrag, waehrung = "EUR") {
+ if (typeof betrag !== "number" || isNaN(betrag)) {
+ throw new TypeError("Ungültiger Betrag: Zahl erwartet");
+ }
+ if (typeof waehrung !== "string" || waehrung.trim().length === 0) {
+ throw new TypeError("Ungültige Währung: nicht-leerer String erwartet");
+ }
+
+ const formatierteZahl = betrag.toFixed(2).replace(".", ",");
+ return `${formatierteZahl} ${waehrung.trim().toUpperCase()}`;
+}
diff --git a/courses/javascript/07_einstieg_typescript/test_aufgabe.js b/courses/javascript/07_einstieg_typescript/test_aufgabe.js
new file mode 100644
index 0000000..34d8aa9
--- /dev/null
+++ b/courses/javascript/07_einstieg_typescript/test_aufgabe.js
@@ -0,0 +1,105 @@
+/**
+ * 🧪 Testsuite für JS/TS 07: TypeScript Type Safety & Interfaces
+ */
+
+// Helper für Exception-Prüfung in Sandbox & Node
+function assertThrows(fn, msg) {
+ let threw = false;
+ try {
+ fn();
+ } catch (e) {
+ threw = true;
+ }
+ assert.ok(threw, msg || "Erwartete Exception wurde nicht geworfen!");
+}
+
+// ----------------------------------------------------
+// 1. Tests für erstelleKunde(id, name, istAktiv, email)
+// ----------------------------------------------------
+const k1 = erstelleKunde(101, "Max Mustermann");
+assert.strictEqual(k1.id, 101, "Kunden-ID sollte 101 sein");
+assert.strictEqual(k1.name, "Max Mustermann", "Name sollte 'Max Mustermann' sein");
+assert.strictEqual(k1.istAktiv, true, "Standardwert für istAktiv sollte true sein");
+assert.ok(typeof k1.erstelltAm === "string" && k1.erstelltAm.length > 0, "erstelltAm muss ein Datums-String sein");
+
+const k2 = erstelleKunde("C-42", " Lisa Schmidt ", false, "lisa@example.com");
+assert.strictEqual(k2.id, "C-42", "Kunden-ID sollte 'C-42' sein");
+assert.strictEqual(k2.name, "Lisa Schmidt", "Name sollte mit .trim() bereinigt werden");
+assert.strictEqual(k2.istAktiv, false, "istAktiv sollte false sein");
+assert.strictEqual(k2.email, "lisa@example.com", "E-Mail sollte übernommen werden");
+
+assertThrows(() => erstelleKunde(null, "Test"), "erstelleKunde mit id=null muss Fehler werfen");
+assertThrows(() => erstelleKunde(10, ""), "erstelleKunde mit leerem Namen muss Fehler werfen");
+assertThrows(() => erstelleKunde(10, " "), "erstelleKunde mit Leerzeichen-Name muss Fehler werfen");
+assertThrows(() => erstelleKunde(10, 12345), "erstelleKunde mit Zahl als Name muss Fehler werfen");
+
+// ----------------------------------------------------
+// 2. Tests für validiereAuftrag(auftragObjekt)
+// ----------------------------------------------------
+const gueltigerAuftrag = {
+ id: "A-1001",
+ kundeId: 101,
+ positionen: [
+ { titel: "Webdesign & Frontend", menge: 1, einzelpreis: 1200.00 },
+ { titel: "Hosting Setup", menge: 2, einzelpreis: 50.00 }
+ ],
+ summe: 1300.00,
+ status: "offen"
+};
+const res1 = validiereAuftrag(gueltigerAuftrag);
+assert.strictEqual(res1.gueltig, true, "Gültiger Auftrag sollte als gueltig: true erkannt werden");
+assert.strictEqual(res1.fehler.length, 0, "Gültiger Auftrag darf keine Fehlermeldungen haben");
+
+const ungueltigerStatus = { ...gueltigerAuftrag, status: "in_prüfung" };
+const res2 = validiereAuftrag(ungueltigerStatus);
+assert.strictEqual(res2.gueltig, false, "Unbekannter Status muss als ungültig erkannt werden");
+
+const keinePositionen = { ...gueltigerAuftrag, positionen: [] };
+const res3 = validiereAuftrag(keinePositionen);
+assert.strictEqual(res3.gueltig, false, "Auftrag ohne Positionen muss ungültig sein");
+
+const fehlerhaftePosition = {
+ ...gueltigerAuftrag,
+ positionen: [{ titel: "", menge: -1, einzelpreis: 10 }]
+};
+const res4 = validiereAuftrag(fehlerhaftePosition);
+assert.strictEqual(res4.gueltig, false, "Ungültige Positionen müssen erkannt werden");
+
+const resNull = validiereAuftrag(null);
+assert.strictEqual(resNull.gueltig, false, "null als Auftrag muss abgefangen werden");
+
+// ----------------------------------------------------
+// 3. Tests für filtereNachStatus(eintraege, zielStatus)
+// ----------------------------------------------------
+const liste = [
+ { id: 1, titel: "Design", status: "offen" },
+ { id: 2, titel: "Backend", status: "in_bearbeitung" },
+ { id: 3, titel: "Deployment", status: "offen" },
+ { id: 4, titel: "Testing", status: "abgeschlossen" }
+];
+const offene = filtereNachStatus(liste, "offen");
+assert.strictEqual(offene.length, 2, "Es sollte genau 2 offene Einträge geben");
+assert.strictEqual(offene[0].id, 1, "Erster gefilterter Eintrag ID");
+assert.strictEqual(offene[1].id, 3, "Zweiter gefilterter Eintrag ID");
+
+const abgeschlossene = filtereNachStatus(liste, "abgeschlossen");
+assert.strictEqual(abgeschlossene.length, 1, "Es sollte 1 abgeschlossenen Eintrag geben");
+
+const stornierte = filtereNachStatus(liste, "storniert");
+assert.strictEqual(stornierte.length, 0, "Nicht vorhandener Status liefert leeres Array");
+
+assert.deepStrictEqual(filtereNachStatus(null, "offen"), [], "Ungültige Liste liefert []");
+
+// ----------------------------------------------------
+// 4. Tests für formatierePreisangabe(betrag, waehrung)
+// ----------------------------------------------------
+assert.strictEqual(formatierePreisangabe(49.99), "49,99 EUR", "formatierePreisangabe(49.99) mit Default EUR");
+assert.strictEqual(formatierePreisangabe(1250, "USD"), "1250,00 USD", "formatierePreisangabe(1250, 'USD')");
+assert.strictEqual(formatierePreisangabe(0, "chf"), "0,00 CHF", "Währung sollte in Großbuchstaben formatiert werden");
+assert.strictEqual(formatierePreisangabe(9.5, "EUR"), "9,50 EUR", "Einstellige Nachkommastelle auf 2 Stellen auffüllen");
+
+assertThrows(() => formatierePreisangabe("49.99"), "String als Betrag muss TypeError werfen");
+assertThrows(() => formatierePreisangabe(NaN), "NaN als Betrag muss TypeError werfen");
+assertThrows(() => formatierePreisangabe(10, ""), "Leere Währung muss TypeError werfen");
+
+console.log("✅ Alle Tests für TS 07 (TypeScript Type Safety & Interfaces) erfolgreich bestanden!");
diff --git a/courses/javascript/08_abschlussprojekt_web_app/README.md b/courses/javascript/08_abschlussprojekt_web_app/README.md
new file mode 100644
index 0000000..ee27a91
--- /dev/null
+++ b/courses/javascript/08_abschlussprojekt_web_app/README.md
@@ -0,0 +1,112 @@
+# 🏆 Modul 08: Master-Abschlussprojekt – Interaktive Task-App (MVC)
+
+> **Leitgedanke:** *„Ein wahrer Software-Engineer zeichnet sich nicht nur dadurch aus, einzelne Funktionen zu schreiben, sondern komplexe Systeme mit klarer Model-View-Architektur, State-Management und 100% testgetriebener Logik zu strukturieren.“*
+
+---
+
+## 📚 Übersicht & Projektziel
+
+Herzlichen Glückwunsch zum Erreichen des **Master-Abschlussprojekts** des JavaScript & TypeScript Kurses! In diesem Abschlussprojekt entwickelst du eine vollständige, professionelle Aufgaben- und Projektmanagement-Web-Applikation.
+
+### Kernanforderungen:
+1. **Saubere Model-View-Trennung (MVC / Clean Architecture)**:
+ - Die Geschäftslogik (`TaskManager`) ist zu 100% unabhängig vom DOM und läuft headless in jedem Test-Runner.
+ - Die View (`TaskApp`) reagiert reaktiv auf State-Änderungen und visualisiert Tasks mit Prioritäts-Badges und Fälligkeitsdaten.
+2. **Umfassende Aufgabenverwaltung**:
+ - Aufgaben hinzufügen mit Titel, Priorität (`high`, `medium`, `low`) und Fälligkeitsdatum (`dueDate`).
+ - Status umschalten (Toggle: Offen <-> Erledigt).
+ - Aufgaben löschen und abgeschlossene Aufgaben gesammelt aufräumen.
+ - Dynamische Filterung (`all`, `active`, `completed`) und Volltextsuche (Live Search).
+ - Aggregat-Statistiken in Echtzeit (Gesamtzahl, Offen, Erledigt, Hohe Priorität).
+3. **100% Unittest-Abdeckung** mit automatisierter Testsuite.
+
+---
+
+## 🏛️ 1. Architektur: Model-View-Controller (MVC)
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ BENUTZER / BROWSER UI │
+└──────────────────────────────┬──────────────────────────────┘
+ │ Klick / Formular-Eingabe
+ ▼
+┌─────────────────────────────────────────────────────────────┐
+│ VIEW / CONTROLLER (app.js) │
+│ - Reagiert auf DOM-Events (submit, click, input) │
+│ - Aktualisiert das DOM bei State-Änderungen │
+│ - Formatiert UI-Elemente (Badges, Datumsanzeige) │
+└──────────────────────────────┬──────────────────────────────┘
+ │ Ruft Methoden auf
+ ▼
+┌─────────────────────────────────────────────────────────────┐
+│ MODEL / STATE (TaskManager) │
+│ - addTask(title, priority, dueDate) │
+│ - toggleTask(id) │
+│ - deleteTask(id) │
+│ - getFilteredTasks(status, query) │
+│ - getStatistics() │
+└─────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## 📐 2. TypeScript Datenmodell & Interfaces
+
+```typescript
+type TaskPriority = "high" | "medium" | "low";
+type FilterStatus = "all" | "active" | "completed";
+
+interface Task {
+ id: number;
+ title: string;
+ priority: TaskPriority;
+ completed: boolean;
+ dueDate: string | null;
+ createdAt: string;
+}
+
+interface TaskStatistics {
+ total: number;
+ active: number;
+ completed: number;
+ highPriorityCount: number;
+}
+```
+
+---
+
+## 🎯 3. Deine Aufgaben in `aufgabe.js`
+
+Implementiere die Klasse `TaskManager`:
+
+### 🎯 TODO 1: `constructor(initialTasks = [])` & `addTask(title, priority = 'medium', dueDate = null)`
+- Erzeugt eine neue Aufgabe mit fortlaufender ID (1, 2, 3, ...).
+- Validiert den Titel (muss ein nicht-leerer String sein; wirft sonst einen `Error`).
+- Setzt Standardwerte für `priority` ('medium') und `dueDate` (null).
+- Gibt das neu erstellte `Task`-Objekt zurück.
+
+### 🎯 TODO 2: `toggleTask(id)`
+- Schaltet `completed` für die Aufgabe mit der angegebenen ID um (`true` <-> `false`).
+- Liefert den aktualisierten Task zurück, oder `null` falls die ID nicht gefunden wurde.
+
+### 🎯 TODO 3: `deleteTask(id)`
+- Entfernt die Aufgabe mit der ID aus der internen Liste.
+- Gibt `true` bei erfolgreichem Löschen zurück, sonst `false`.
+
+### 🎯 TODO 4: `getFilteredTasks(filterStatus = 'all', searchQuery = '')`
+- Filtert nach Status: `'all'` (alle), `'active'` (nur unerledigte), `'completed'` (nur erledigte).
+- Filtert optional case-insensitive nach Teilübereinstimmung im Titel (`searchQuery`).
+- Gibt eine Kopie des gefilterten Arrays zurück.
+
+### 🎯 TODO 5: `getStatistics()`
+- Liefert ein Objekt mit `{ total, active, completed, highPriorityCount }`.
+
+---
+
+## 🧪 4. Verifikation & Ausführung
+
+### Unittests ausführen:
+Öffne das Modul in der integrierten Web-IDE oder führe `test_aufgabe.js` aus.
+
+### Interaktive Browser-App testen:
+Öffne `index.html` direkt im Browser, um die live gerenderte Task-App auszuprobieren!
diff --git a/courses/javascript/08_abschlussprojekt_web_app/app.js b/courses/javascript/08_abschlussprojekt_web_app/app.js
new file mode 100644
index 0000000..70478f8
--- /dev/null
+++ b/courses/javascript/08_abschlussprojekt_web_app/app.js
@@ -0,0 +1,295 @@
+/**
+ * 🌐 MASTER 08: INTERAKTIVE TASK-APP (VIEW & CONTROLLER) 🌐
+ * ==========================================================
+ * Verbindet die TaskManager-Geschäftslogik mit einer modernen,
+ * reaktiven Benutzeroberfläche im Browser.
+ */
+
+// Falls TaskManager noch nicht global vorhanden ist (z.B. Standalone):
+if (typeof TaskManager === "undefined") {
+ // Fallback Minimal-Implementation oder Import
+ console.warn("TaskManager nicht gefunden. Lade Fallback-Definition.");
+}
+
+class TaskApp {
+ constructor(rootElementId = "app") {
+ this.root = document.getElementById(rootElementId) || document.body;
+ this.manager = new TaskManager([
+ { id: 1, title: "TypeScript Interfaces & Union Types verstehen", priority: "high", completed: true, dueDate: "2026-08-30", createdAt: new Date().toISOString() },
+ { id: 2, title: "Model-View-Controller Architektur im Web umsetzen", priority: "high", completed: false, dueDate: "2026-09-01", createdAt: new Date().toISOString() },
+ { id: 3, title: "Design System & Dark Mode Akzente optimieren", priority: "medium", completed: false, dueDate: "2026-09-05", createdAt: new Date().toISOString() },
+ { id: 4, title: "Automatisierte Unittests mit 100% Abdeckung schreiben", priority: "low", completed: false, dueDate: null, createdAt: new Date().toISOString() }
+ ]);
+
+ this.currentFilter = "all";
+ this.searchQuery = "";
+
+ this.init();
+ }
+
+ init() {
+ this.renderLayout();
+ this.attachEventListeners();
+ this.updateView();
+ }
+
+ renderLayout() {
+ this.root.innerHTML = `
+
+
+
+
+
+
+ 🎯 Modern Task & Project Planner
+
+
Statussichere Aufgabenverwaltung mit TypeScript & Clean Architecture
+
+
+ 🧹 Erledigte aufräumen
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Alle
+ Aktiv
+ Erledigt
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ }
+
+ attachEventListeners() {
+ // Formular-Submit
+ const form = document.getElementById("task-form");
+ if (form) {
+ form.addEventListener("submit", (e) => {
+ e.preventDefault();
+ const titleInput = document.getElementById("task-input-title");
+ const prioritySelect = document.getElementById("task-select-priority");
+ const dueInput = document.getElementById("task-input-due");
+
+ const title = titleInput.value.trim();
+ const priority = prioritySelect.value;
+ const dueDate = dueInput.value || null;
+
+ if (title) {
+ try {
+ this.manager.addTask(title, priority, dueDate);
+ titleInput.value = "";
+ dueInput.value = "";
+ this.updateView();
+ } catch (err) {
+ alert(err.message);
+ }
+ }
+ });
+ }
+
+ // Filter Buttons
+ const filterContainer = document.getElementById("filter-buttons");
+ if (filterContainer) {
+ filterContainer.addEventListener("click", (e) => {
+ const btn = e.target.closest("button[data-filter]");
+ if (btn) {
+ this.currentFilter = btn.getAttribute("data-filter");
+ // Button-Stile aktualisieren
+ filterContainer.querySelectorAll(".filter-btn").forEach((b) => {
+ b.style.background = "white";
+ b.style.color = "#475569";
+ b.style.borderColor = "#cbd5e1";
+ });
+ btn.style.background = "#4f46e5";
+ btn.style.color = "white";
+ btn.style.borderColor = "#4f46e5";
+ this.updateView();
+ }
+ });
+ }
+
+ // Live-Suche
+ const searchInput = document.getElementById("search-input");
+ if (searchInput) {
+ searchInput.addEventListener("input", (e) => {
+ this.searchQuery = e.target.value;
+ this.updateView();
+ });
+ }
+
+ // Erledigte aufräumen
+ const clearBtn = document.getElementById("btn-clear-completed");
+ if (clearBtn) {
+ clearBtn.addEventListener("click", () => {
+ if (confirm("Möchtest du alle erledigten Aufgaben wirklich entfernen?")) {
+ this.manager.clearCompleted();
+ this.updateView();
+ }
+ });
+ }
+
+ // Task-Liste Delegation (Toggle & Delete)
+ const listContainer = document.getElementById("task-list");
+ if (listContainer) {
+ listContainer.addEventListener("click", (e) => {
+ const target = e.target;
+ const taskCard = target.closest("[data-task-id]");
+ if (!taskCard) return;
+
+ const id = Number(taskCard.getAttribute("data-task-id"));
+
+ if (target.classList.contains("btn-delete") || target.closest(".btn-delete")) {
+ this.manager.deleteTask(id);
+ this.updateView();
+ } else if (target.classList.contains("task-checkbox")) {
+ this.manager.toggleTask(id);
+ this.updateView();
+ }
+ });
+ }
+ }
+
+ updateView() {
+ this.renderStats();
+ this.renderTasks();
+ }
+
+ renderStats() {
+ const stats = this.manager.getStatistics();
+ const elTotal = document.getElementById("stat-total");
+ const elActive = document.getElementById("stat-active");
+ const elCompleted = document.getElementById("stat-completed");
+ const elHigh = document.getElementById("stat-high");
+
+ if (elTotal) elTotal.textContent = stats.total;
+ if (elActive) elActive.textContent = stats.active;
+ if (elCompleted) elCompleted.textContent = stats.completed;
+ if (elHigh) elHigh.textContent = stats.highPriorityCount;
+ }
+
+ renderTasks() {
+ const container = document.getElementById("task-list");
+ if (!container) return;
+
+ const filtered = this.manager.getFilteredTasks(this.currentFilter, this.searchQuery);
+
+ if (filtered.length === 0) {
+ container.innerHTML = `
+
+
✨
+
Keine Aufgaben gefunden
+
Alle Aufgaben erledigt oder kein Treffer für den aktuellen Filter.
+
+ `;
+ return;
+ }
+
+ container.innerHTML = filtered
+ .map((t) => {
+ const priorityColors = {
+ high: { bg: "#fee2e2", text: "#991b1b", label: "🔥 Hoch" },
+ medium: { bg: "#fef3c7", text: "#92400e", label: "⚡ Mittel" },
+ low: { bg: "#dcfce7", text: "#166534", label: "🌱 Niedrig" }
+ };
+ const p = priorityColors[t.priority] || priorityColors.medium;
+
+ const dueHtml = t.dueDate
+ ? `
+ 📅 ${t.dueDate}
+ `
+ : "";
+
+ return `
+
+
+
+
+
+ ${this.escapeHtml(t.title)}
+
+
+
+ ${p.label}
+
+ ${dueHtml}
+
+
+
+
+ 🗑️
+
+
+ `;
+ })
+ .join("");
+ }
+
+ escapeHtml(str) {
+ if (!str) return "";
+ return str
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+ }
+}
+
+// Bei DOMContentLoaded automatisch starten wenn im Browser
+if (typeof document !== "undefined") {
+ document.addEventListener("DOMContentLoaded", () => {
+ if (document.getElementById("app") || document.getElementById("task-app-root")) {
+ new TaskApp(document.getElementById("task-app-root") ? "task-app-root" : "app");
+ }
+ });
+}
diff --git a/courses/javascript/08_abschlussprojekt_web_app/aufgabe.js b/courses/javascript/08_abschlussprojekt_web_app/aufgabe.js
new file mode 100644
index 0000000..40369e6
--- /dev/null
+++ b/courses/javascript/08_abschlussprojekt_web_app/aufgabe.js
@@ -0,0 +1,145 @@
+/**
+ * 🌐 MASTER 08: INTERAKTIVE TASK-APP (STATE & BUSINESS LOGIC) 🌐
+ * ===============================================================
+ * In diesem Master-Abschlussprojekt erstellst du die vollständige
+ * Geschäftslogik (Model & State Manager) für eine interaktive
+ * Aufgabenverwaltungs-Web-App mit sauberer Model-View Trennung.
+ */
+
+/**
+ * @typedef {'high' | 'medium' | 'low'} TaskPriority
+ * @typedef {'all' | 'active' | 'completed'} FilterStatus
+ *
+ * @typedef {Object} Task
+ * @property {number} id - Eindeutige numerische ID
+ * @property {string} title - Titel der Aufgabe
+ * @property {TaskPriority} priority - Priorität ('high', 'medium', 'low')
+ * @property {boolean} completed - Erledigungsstatus
+ * @property {string | null} dueDate - Optionales Fälligkeitsdatum (z.B. '2026-12-31')
+ * @property {string} createdAt - ISO-Datumsstring
+ *
+ * @typedef {Object} TaskStatistics
+ * @property {number} total - Gesamtzahl aller Aufgaben
+ * @property {number} active - Anzahl aktiver (unerledigter) Aufgaben
+ * @property {number} completed - Anzahl erledigter Aufgaben
+ * @property {number} highPriorityCount - Anzahl Aufgaben mit Priorität 'high'
+ */
+
+class TaskManager {
+ constructor(initialTasks = []) {
+ /** @type {Task[]} */
+ this.tasks = [];
+ this.nextId = 1;
+
+ // TODO: Initialisiere das Array und lade eventuelle initialTasks
+ }
+
+ /**
+ * 🎯 TEILZIEL 1 (TODO 1): addTask(title, priority = 'medium', dueDate = null)
+ * Fügt eine neue Aufgabe hinzu.
+ *
+ * Anforderungen:
+ * - title: string (nach .trim() nicht leer, sonst Error werfen)
+ * - priority: 'high' | 'medium' | 'low' (Standard: 'medium')
+ * - dueDate: string | null (Standard: null)
+ * - completed: initial false
+ * - createdAt: new Date().toISOString()
+ * - Vergibt eine eindeutige, inkrementierte ID (1, 2, 3, ...)
+ * - Gibt das erstellte Task-Objekt zurück.
+ *
+ * @param {string} title
+ * @param {TaskPriority} [priority='medium']
+ * @param {string | null} [dueDate=null]
+ * @returns {Task}
+ */
+ addTask(title, priority = "medium", dueDate = null) {
+ // TODO: Neue Aufgabe erstellen und im tasks-Array speichern
+ return null;
+ }
+
+ /**
+ * 🎯 TEILZIEL 2 (TODO 2): toggleTask(id)
+ * Schaltet den completed-Status der Aufgabe mit der übergebenen ID um (true <-> false).
+ *
+ * Anforderungen:
+ * - Findet die Aufgabe anhand der ID.
+ * - Schaltet task.completed um.
+ * - Gibt den aktualisierten Task zurück, oder null falls ID nicht existiert.
+ *
+ * @param {number} id
+ * @returns {Task | null}
+ */
+ toggleTask(id) {
+ // TODO: Status umschalten
+ return null;
+ }
+
+ /**
+ * 🎯 TEILZIEL 3 (TODO 3): deleteTask(id)
+ * Entfernt eine Aufgabe anhand ihrer ID.
+ *
+ * Anforderungen:
+ * - Entfernt die Aufgabe aus this.tasks.
+ * - Gibt true zurück wenn gelöscht wurde, sonst false wenn ID nicht vorhanden war.
+ *
+ * @param {number} id
+ * @returns {boolean}
+ */
+ deleteTask(id) {
+ // TODO: Aufgabe löschen
+ return false;
+ }
+
+ /**
+ * 🎯 TEILZIEL 4 (TODO 4): getFilteredTasks(filterStatus = 'all', searchQuery = '')
+ * Liefert gefilterte Aufgaben basierend auf Status und optionalem Suchbegriff.
+ *
+ * Anforderungen:
+ * - filterStatus:
+ * - 'all': Alle Aufgaben
+ * - 'active': Nur Aufgaben mit completed === false
+ * - 'completed': Nur Aufgaben mit completed === true
+ * - searchQuery: Filtert zusätzlich case-insensitive nach Teilübereinstimmung im title (wenn nicht leer)
+ * - Liefert eine neue Array-Kopie der gefilterten Aufgaben zurück.
+ *
+ * @param {FilterStatus} [filterStatus='all']
+ * @param {string} [searchQuery='']
+ * @returns {Task[]}
+ */
+ getFilteredTasks(filterStatus = "all", searchQuery = "") {
+ // TODO: Filtern nach Status und Volltextsuche
+ return [];
+ }
+
+ /**
+ * 🎯 TEILZIEL 5 (TODO 5): getStatistics()
+ * Berechnet Aggregat-Statistiken über alle Aufgaben.
+ *
+ * Anforderungen:
+ * - total: Gesamtzahl aller Aufgaben
+ * - active: Anzahl noch nicht erledigter Aufgaben
+ * - completed: Anzahl erledigter Aufgaben
+ * - highPriorityCount: Anzahl aller Aufgaben mit priority === 'high'
+ *
+ * @returns {TaskStatistics}
+ */
+ getStatistics() {
+ // TODO: Statistiken berechnen
+ return {
+ total: 0,
+ active: 0,
+ completed: 0,
+ highPriorityCount: 0
+ };
+ }
+
+ /**
+ * Hilfsmethode: Findet eine Aufgabe nach ID
+ * @param {number} id
+ * @returns {Task | null}
+ */
+ getTaskById(id) {
+ const numId = Number(id);
+ return this.tasks.find(t => t.id === numId) || null;
+ }
+}
diff --git a/courses/javascript/08_abschlussprojekt_web_app/index.html b/courses/javascript/08_abschlussprojekt_web_app/index.html
new file mode 100644
index 0000000..9b1b63e
--- /dev/null
+++ b/courses/javascript/08_abschlussprojekt_web_app/index.html
@@ -0,0 +1,179 @@
+
+
+
+
+
+ Master 08: Interaktive Task-App (MVC & Clean Architecture)
+
+
+
+
+
+
+
+
+
+
+
+ 📂 Dateien dieser Einheit:
+
+
+
+
+
+
+ 1. 🏆 Dein Meisterstück: Das Master-Abschlussprojekt
+
+ Herzlichen Glückwunsch! Du hast moderne JavaScript-Grundlagen, funktionale Array-Transformationen und statische Typsicherheit mit TypeScript gemeistert.
+
+
+ In diesem Master-Abschlussprojekt vereinst du alle Konzepte in einer professionell strukturierten Aufgabenverwaltungs-App. Das Projekt folgt dem Model-View-Controller (MVC) -Muster, dem Industriestandard moderner Web-Architektur.
+
+
+
+
+
🌱 1. ES6+ & Arrays
+
+ Unveränderliche Arrays, Filterung mit .filter(), Suchen mit .find() / .findIndex() und Template Strings.
+
+
+
+
🛡️ 2. Type Safety & Interfaces
+
+ Definierte Datenstrukturen (TaskPriority, FilterStatus, TaskStatistics) und robuste Input-Validierung.
+
+
+
+
🚀 3. State & UI-Sync
+
+ Zentrales State Management in der Klasse TaskManager, Event-Delegation und 100% Unittest-Abdeckung!
+
+
+
+
+
+
+
+ 2. 🏛️ Die Architektur: Model-View-Controller (MVC)
+
+ Strikte Trennung von Geschäftslogik und Anzeige verhindert schwer wartbaren Code („Spaghetti-Code“):
+
+
+
+
+
1. Das Model (State & Logik)
+
Klasse TaskManager
+
+ Verwaltet das interne tasks-Array, vergibt IDs, schaltet Status um, filtert nach Kriterien und berechnet Aggregat-Statistiken.
+
+
+ 💡 100% Headless: Funktioniert komplett ohne DOM im Testrunner!
+
+
+
+
+
2. Die View (DOM-Renderer)
+
HTML-Widgets & Badges
+
+ Rendert Formular, Status-Karten (Zähler), Filter-Buttons, Suchleiste und Task-Karten mit Prioritätsfarben.
+
+
+ 👀 Dumb View: Zeigt nur den aktuellen State an.
+
+
+
+
+
3. Der Controller (Event-Handler)
+
Klasse TaskApp
+
+ Lauscht auf Klicks, Tastatur-Eingaben und Form-Submits, delegiert Aktionen an den TaskManager und löst Neuzeichnungen aus.
+
+
+ 🛡️ Sicher: Fängt Fehleingaben über Exceptions ab.
+
+
+
+
+
+
+
+ 3. 🚀 Interaktive Live-Applikation
+ So sieht die fertige Anwendung aus – teste sie hier direkt interaktiv aus:
+
+
+
+
+
+
+ 4. 🎯 Aufgaben & Checkliste
+ Öffne die Datei aufgabe.js und implementiere die TaskManager-Klasse:
+
+
+
+
+ TODO 1: addTask(title, priority, dueDate) – Validierung, ID-Vergabe (1, 2, ...), Default-Werte und Rückgabe des Tasks.
+
+
+
+
+
+
+ TODO 2: toggleTask(id) – Umschalten von task.completed (true <-> false) und Rückgabe oder null.
+
+
+
+
+
+
+ TODO 3: deleteTask(id) – Entfernen aus dem Array, Rückgabe true bei Erfolg, sonst false.
+
+
+
+
+
+
+ TODO 4: getFilteredTasks(filterStatus, searchQuery) – Filter nach 'all'/'active'/'completed' + Volltextsuche im Titel.
+
+
+
+
+
+
+ TODO 5: getStatistics() – Rückgabe von { total, active, completed, highPriorityCount }.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/courses/javascript/08_abschlussprojekt_web_app/musterloesung.js b/courses/javascript/08_abschlussprojekt_web_app/musterloesung.js
new file mode 100644
index 0000000..6ff8c96
--- /dev/null
+++ b/courses/javascript/08_abschlussprojekt_web_app/musterloesung.js
@@ -0,0 +1,154 @@
+/**
+ * 🌐 MASTER 08: INTERAKTIVE TASK-APP (MUSTERLÖSUNG) 🌐
+ * ====================================================
+ */
+
+/**
+ * @typedef {'high' | 'medium' | 'low'} TaskPriority
+ * @typedef {'all' | 'active' | 'completed'} FilterStatus
+ *
+ * @typedef {Object} Task
+ * @property {number} id
+ * @property {string} title
+ * @property {TaskPriority} priority
+ * @property {boolean} completed
+ * @property {string | null} dueDate
+ * @property {string} createdAt
+ *
+ * @typedef {Object} TaskStatistics
+ * @property {number} total
+ * @property {number} active
+ * @property {number} completed
+ * @property {number} highPriorityCount
+ */
+
+class TaskManager {
+ constructor(initialTasks = []) {
+ /** @type {Task[]} */
+ this.tasks = [];
+ this.nextId = 1;
+
+ if (Array.isArray(initialTasks)) {
+ for (const t of initialTasks) {
+ if (t && typeof t === "object") {
+ const task = {
+ id: typeof t.id === "number" ? t.id : this.nextId++,
+ title: typeof t.title === "string" ? t.title.trim() : "Ohne Titel",
+ priority: ["high", "medium", "low"].includes(t.priority) ? t.priority : "medium",
+ completed: Boolean(t.completed),
+ dueDate: (typeof t.dueDate === "string" && t.dueDate.trim().length > 0) ? t.dueDate.trim() : null,
+ createdAt: t.createdAt || new Date().toISOString()
+ };
+ if (task.id >= this.nextId) {
+ this.nextId = task.id + 1;
+ }
+ this.tasks.push(task);
+ }
+ }
+ }
+ }
+
+ /**
+ * 🎯 TEILZIEL 1: addTask(title, priority = 'medium', dueDate = null)
+ */
+ addTask(title, priority = "medium", dueDate = null) {
+ if (typeof title !== "string" || title.trim().length === 0) {
+ throw new Error("Aufgabentitel darf nicht leer sein");
+ }
+
+ const validPriority = ["high", "medium", "low"].includes(priority) ? priority : "medium";
+ const normalizedDueDate = (typeof dueDate === "string" && dueDate.trim().length > 0) ? dueDate.trim() : null;
+
+ const task = {
+ id: this.nextId++,
+ title: title.trim(),
+ priority: validPriority,
+ completed: false,
+ dueDate: normalizedDueDate,
+ createdAt: new Date().toISOString()
+ };
+
+ this.tasks.push(task);
+ return task;
+ }
+
+ /**
+ * 🎯 TEILZIEL 2: toggleTask(id)
+ */
+ toggleTask(id) {
+ const numId = Number(id);
+ const task = this.tasks.find(t => t.id === numId);
+ if (!task) {
+ return null;
+ }
+ task.completed = !task.completed;
+ return task;
+ }
+
+ /**
+ * 🎯 TEILZIEL 3: deleteTask(id)
+ */
+ deleteTask(id) {
+ const numId = Number(id);
+ const index = this.tasks.findIndex(t => t.id === numId);
+ if (index === -1) {
+ return false;
+ }
+ this.tasks.splice(index, 1);
+ return true;
+ }
+
+ /**
+ * 🎯 TEILZIEL 4: getFilteredTasks(filterStatus = 'all', searchQuery = '')
+ */
+ getFilteredTasks(filterStatus = "all", searchQuery = "") {
+ let result = [...this.tasks];
+
+ if (filterStatus === "active") {
+ result = result.filter(t => !t.completed);
+ } else if (filterStatus === "completed") {
+ result = result.filter(t => t.completed);
+ }
+
+ if (typeof searchQuery === "string" && searchQuery.trim().length > 0) {
+ const query = searchQuery.trim().toLowerCase();
+ result = result.filter(t => t.title.toLowerCase().includes(query));
+ }
+
+ return result;
+ }
+
+ /**
+ * 🎯 TEILZIEL 5: getStatistics()
+ */
+ getStatistics() {
+ const total = this.tasks.length;
+ const completed = this.tasks.filter(t => t.completed).length;
+ const active = total - completed;
+ const highPriorityCount = this.tasks.filter(t => t.priority === "high").length;
+
+ return {
+ total,
+ active,
+ completed,
+ highPriorityCount
+ };
+ }
+
+ /**
+ * Hilfsmethode: Task nach ID suchen
+ */
+ getTaskById(id) {
+ const numId = Number(id);
+ return this.tasks.find(t => t.id === numId) || null;
+ }
+
+ /**
+ * Hilfsmethode: Alle erledigten Aufgaben auf einmal löschen
+ */
+ clearCompleted() {
+ const initialCount = this.tasks.length;
+ this.tasks = this.tasks.filter(t => !t.completed);
+ return initialCount - this.tasks.length;
+ }
+}
diff --git a/courses/javascript/08_abschlussprojekt_web_app/test_aufgabe.js b/courses/javascript/08_abschlussprojekt_web_app/test_aufgabe.js
new file mode 100644
index 0000000..b0df3ef
--- /dev/null
+++ b/courses/javascript/08_abschlussprojekt_web_app/test_aufgabe.js
@@ -0,0 +1,111 @@
+/**
+ * 🧪 Testsuite für Master 08: Interaktive Task-App (TaskManager)
+ */
+
+function assertThrows(fn, msg) {
+ let threw = false;
+ try {
+ fn();
+ } catch (e) {
+ threw = true;
+ }
+ assert.ok(threw, msg || "Erwarteter Fehler wurde nicht geworfen!");
+}
+
+// Instanz erzeugen
+const manager = new TaskManager();
+
+// ----------------------------------------------------
+// 1. Tests für addTask(title, priority, dueDate)
+// ----------------------------------------------------
+const t1 = manager.addTask("TypeScript Typen & Interfaces lernen", "high", "2026-12-31");
+assert.strictEqual(t1.id, 1, "Erste Aufgabe sollte ID 1 erhalten");
+assert.strictEqual(t1.title, "TypeScript Typen & Interfaces lernen", "Titel korrekt");
+assert.strictEqual(t1.priority, "high", "Priorität sollte 'high' sein");
+assert.strictEqual(t1.completed, false, "Initialer Status muss completed: false sein");
+assert.strictEqual(t1.dueDate, "2026-12-31", "Fälligkeitsdatum korrekt");
+assert.ok(typeof t1.createdAt === "string", "createdAt muss gesetzt sein");
+
+const t2 = manager.addTask(" CSS Design System verfeinern ");
+assert.strictEqual(t2.id, 2, "Zweite Aufgabe sollte ID 2 erhalten");
+assert.strictEqual(t2.title, "CSS Design System verfeinern", "Titel muss getrimmt sein");
+assert.strictEqual(t2.priority, "medium", "Standard-Priorität sollte 'medium' sein");
+assert.strictEqual(t2.dueDate, null, "Ohne Fälligkeitsdatum sollte null gesetzt werden");
+
+const t3 = manager.addTask("Unit Tests für State-Manager schreiben", "low");
+assert.strictEqual(t3.id, 3, "Dritte Aufgabe sollte ID 3 erhalten");
+
+const t4 = manager.addTask("Master Release v1.0 bereitstellen", "high", "2026-09-01");
+assert.strictEqual(t4.id, 4, "Vierte Aufgabe sollte ID 4 erhalten");
+
+// Validierung von fehlerhaftem Input
+assertThrows(() => manager.addTask(""), "Leerer Titel muss Error werfen");
+assertThrows(() => manager.addTask(" "), "Whitespace-Titel muss Error werfen");
+assertThrows(() => manager.addTask(null), "null als Titel muss Error werfen");
+
+// ----------------------------------------------------
+// 2. Tests für toggleTask(id)
+// ----------------------------------------------------
+const toggled1 = manager.toggleTask(1);
+assert.strictEqual(toggled1.completed, true, "Task 1 sollte nach erstem Toggle completed: true sein");
+
+const toggledAgain = manager.toggleTask(1);
+assert.strictEqual(toggledAgain.completed, false, "Task 1 sollte nach zweitem Toggle wieder completed: false sein");
+
+const toggledNonExistent = manager.toggleTask(999);
+assert.strictEqual(toggledNonExistent, null, "Toggle für unbekannte ID sollte null liefern");
+
+// Für nachfolgende Tests Task 2 auf completed setzen
+manager.toggleTask(2);
+
+// ----------------------------------------------------
+// 3. Tests für deleteTask(id)
+// ----------------------------------------------------
+const deleted = manager.deleteTask(3);
+assert.strictEqual(deleted, true, "deleteTask(3) sollte true zurückgeben");
+assert.strictEqual(manager.tasks.length, 3, "Nach Löschen sollten noch 3 Tasks vorhanden sein");
+
+const deletedAgain = manager.deleteTask(3);
+assert.strictEqual(deletedAgain, false, "Erneutes Löschen derselben ID sollte false liefern");
+
+// ----------------------------------------------------
+// 4. Tests für getFilteredTasks(filterStatus, searchQuery)
+// ----------------------------------------------------
+// Aktueller Zustand:
+// - Task 1: "TypeScript ...", high, completed: false
+// - Task 2: "CSS Design ...", medium, completed: true
+// - Task 4: "Master Release ...", high, completed: false
+
+const allTasks = manager.getFilteredTasks("all");
+assert.strictEqual(allTasks.length, 3, "Filter 'all' sollte alle 3 Tasks liefern");
+
+const activeTasks = manager.getFilteredTasks("active");
+assert.strictEqual(activeTasks.length, 2, "Filter 'active' sollte 2 Tasks liefern");
+assert.strictEqual(activeTasks[0].id, 1, "Erster aktiver Task");
+assert.strictEqual(activeTasks[1].id, 4, "Zweiter aktiver Task");
+
+const completedTasks = manager.getFilteredTasks("completed");
+assert.strictEqual(completedTasks.length, 1, "Filter 'completed' sollte 1 Task liefern");
+assert.strictEqual(completedTasks[0].id, 2, "Erledigter Task ID");
+
+// Volltextsuche
+const searchResult = manager.getFilteredTasks("all", "release");
+assert.strictEqual(searchResult.length, 1, "Suche nach 'release' sollte Task 4 finden");
+assert.strictEqual(searchResult[0].id, 4, "Gefundene Task-ID");
+
+const searchCaseInsensitive = manager.getFilteredTasks("all", "TYPESCRIPT");
+assert.strictEqual(searchCaseInsensitive.length, 1, "Suche sollte case-insensitive sein");
+
+const searchEmpty = manager.getFilteredTasks("all", "NICHT_VORHANDEN_123");
+assert.strictEqual(searchEmpty.length, 0, "Erfolglose Suche liefert leeres Array");
+
+// ----------------------------------------------------
+// 5. Tests für getStatistics()
+// ----------------------------------------------------
+const stats = manager.getStatistics();
+assert.strictEqual(stats.total, 3, "Gesamtzahl Tasks");
+assert.strictEqual(stats.active, 2, "Aktive Tasks");
+assert.strictEqual(stats.completed, 1, "Erledigte Tasks");
+assert.strictEqual(stats.highPriorityCount, 2, "Hohe Priorität Tasks (Task 1 & Task 4)");
+
+console.log("✅ Alle Tests für Master 08 (Interaktive Task-App / TaskManager) erfolgreich bestanden!");
diff --git a/courses/javascript/course.json b/courses/javascript/course.json
index e307fa2..dd66c84 100644
--- a/courses/javascript/course.json
+++ b/courses/javascript/course.json
@@ -4,16 +4,38 @@
"language": "javascript",
"icon": "🌐",
"runner": "javascript",
- "description": "Vom Browser-Scripting über ES6-Funktionen, Arrays bis hin zu asynchronem JavaScript.",
+ "description": "Vom Browser-Scripting über ES6-Funktionen, Arrays bis hin zu TypeScript Type Safety und dem Master-Abschlussprojekt.",
"tracks": [
{
"id": "track_1_grundlagen",
"title": "🌱 Lehrpfad 1: JavaScript Grundlagen & ES6",
- "description": "Variablen (let/const), Template Strings, Arrow Functions und Array-Methoden.",
+ "description": "Variablen, Template Strings, Arrow Functions, Arrays, Objekte, Destructuring und DOM-Manipulation & Events.",
"certificateKey": "js_grundlagen",
"chapters": [
{ "folder": "01_erste_schritte_variablen", "title": "JS 01: Variablen, Typen & Template-Strings", "taskFile": "aufgabe.js", "testFile": "test_aufgabe.js" },
- { "folder": "02_funktionen_und_arrays", "title": "JS 02: Arrow Functions, Arrays & Map/Filter", "taskFile": "aufgabe.js", "testFile": "test_aufgabe.js" }
+ { "folder": "02_funktionen_und_arrays", "title": "JS 02: Arrow Functions, Arrays & Map/Filter", "taskFile": "aufgabe.js", "testFile": "test_aufgabe.js" },
+ { "folder": "03_objekte_und_destructuring", "title": "JS 03: Objekte, Destructuring & Rest/Spread", "taskFile": "aufgabe.js", "testFile": "test_aufgabe.js" },
+ { "folder": "04_dom_und_events", "title": "JS 04: DOM-Manipulation & Events", "taskFile": "aufgabe.js", "testFile": "test_aufgabe.js" }
+ ]
+ },
+ {
+ "id": "track_2_async_und_apis",
+ "title": "⚡ Lehrpfad 2: Asynchrones JavaScript, Promises & REST-APIs",
+ "description": "Callbacks, Event Loop, Promises, Chaining, Promise.all, Async/Await und REST-APIs mit JSON.",
+ "certificateKey": "js_async",
+ "chapters": [
+ { "folder": "05_callbacks_und_promises", "title": "JS 05: Callbacks, Event Loop & Promises", "taskFile": "aufgabe.js", "testFile": "test_aufgabe.js" },
+ { "folder": "06_async_await_und_fetch", "title": "JS 06: Async/Await & REST-APIs (Fetch)", "taskFile": "aufgabe.js", "testFile": "test_aufgabe.js" }
+ ]
+ },
+ {
+ "id": "track_3_typescript_und_master",
+ "title": "🛡️ Lehrpfad 3: TypeScript & Master-Abschlussprojekt",
+ "description": "Statische Typsicherheit, Interfaces, Generics, State-Management und vollwertige Web-Applikation.",
+ "certificateKey": "js_master",
+ "chapters": [
+ { "folder": "07_einstieg_typescript", "title": "TS 07: TypeScript Type Safety & Interfaces", "taskFile": "aufgabe.js", "testFile": "test_aufgabe.js" },
+ { "folder": "08_abschlussprojekt_web_app", "title": "Master 08: Interaktive Task-App (MVC)", "taskFile": "aufgabe.js", "testFile": "test_aufgabe.js" }
]
}
]