From 1916a634f49c61cffe99eb237c7492e8aade3f21 Mon Sep 17 00:00:00 2001 From: Alex Mueller Date: Sun, 30 Aug 2026 00:42:16 +0200 Subject: [PATCH] =?UTF-8?q?Vollst=C3=A4ndiges=20JavaScript=20&=20TypeScrip?= =?UTF-8?q?t=20Curriculum=20(8=20Module=20=C3=BCber=203=20Lehrpfade:=20ES6?= =?UTF-8?q?=20Grundlagen,=20Asynchrones=20JS=20&=20Fetch,=20TypeScript=20&?= =?UTF-8?q?=20Web-App=20Abschlussprojekt)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gamification.json | 2 +- assets/certificate.js | 20 +- assets/gamification_data.js | 2 +- assets/runners/js_runner.js | 47 ++- .../01_erste_schritte_variablen/index.html | 3 +- .../02_funktionen_und_arrays/index.html | 4 +- .../03_objekte_und_destructuring/README.md | 118 +++++++ .../03_objekte_und_destructuring/aufgabe.js | 46 +++ .../03_objekte_und_destructuring/index.html | 171 ++++++++++ .../musterloesung.js | 27 ++ .../test_aufgabe.js | 83 +++++ .../javascript/04_dom_und_events/README.md | 143 +++++++++ .../javascript/04_dom_und_events/aufgabe.js | 48 +++ .../javascript/04_dom_und_events/index.html | 164 ++++++++++ .../04_dom_und_events/musterloesung.js | 40 +++ .../04_dom_und_events/test_aufgabe.js | 127 ++++++++ .../05_callbacks_und_promises/README.md | 131 ++++++++ .../05_callbacks_und_promises/aufgabe.js | 44 +++ .../05_callbacks_und_promises/index.html | 203 ++++++++++++ .../musterloesung.js | 42 +++ .../05_callbacks_und_promises/test_aufgabe.js | 78 +++++ .../06_async_await_und_fetch/README.md | 142 +++++++++ .../06_async_await_und_fetch/aufgabe.js | 42 +++ .../06_async_await_und_fetch/index.html | 189 +++++++++++ .../06_async_await_und_fetch/musterloesung.js | 50 +++ .../06_async_await_und_fetch/test_aufgabe.js | 138 ++++++++ .../07_einstieg_typescript/README.md | 151 +++++++++ .../07_einstieg_typescript/aufgabe.js | 122 ++++++++ .../07_einstieg_typescript/index.html | 175 +++++++++++ .../07_einstieg_typescript/musterloesung.js | 132 ++++++++ .../07_einstieg_typescript/test_aufgabe.js | 105 +++++++ .../08_abschlussprojekt_web_app/README.md | 112 +++++++ .../08_abschlussprojekt_web_app/app.js | 295 ++++++++++++++++++ .../08_abschlussprojekt_web_app/aufgabe.js | 145 +++++++++ .../08_abschlussprojekt_web_app/index.html | 179 +++++++++++ .../musterloesung.js | 154 +++++++++ .../test_aufgabe.js | 111 +++++++ courses/javascript/course.json | 28 +- 38 files changed, 3799 insertions(+), 14 deletions(-) create mode 100644 courses/javascript/03_objekte_und_destructuring/README.md create mode 100644 courses/javascript/03_objekte_und_destructuring/aufgabe.js create mode 100644 courses/javascript/03_objekte_und_destructuring/index.html create mode 100644 courses/javascript/03_objekte_und_destructuring/musterloesung.js create mode 100644 courses/javascript/03_objekte_und_destructuring/test_aufgabe.js create mode 100644 courses/javascript/04_dom_und_events/README.md create mode 100644 courses/javascript/04_dom_und_events/aufgabe.js create mode 100644 courses/javascript/04_dom_und_events/index.html create mode 100644 courses/javascript/04_dom_und_events/musterloesung.js create mode 100644 courses/javascript/04_dom_und_events/test_aufgabe.js create mode 100644 courses/javascript/05_callbacks_und_promises/README.md create mode 100644 courses/javascript/05_callbacks_und_promises/aufgabe.js create mode 100644 courses/javascript/05_callbacks_und_promises/index.html create mode 100644 courses/javascript/05_callbacks_und_promises/musterloesung.js create mode 100644 courses/javascript/05_callbacks_und_promises/test_aufgabe.js create mode 100644 courses/javascript/06_async_await_und_fetch/README.md create mode 100644 courses/javascript/06_async_await_und_fetch/aufgabe.js create mode 100644 courses/javascript/06_async_await_und_fetch/index.html create mode 100644 courses/javascript/06_async_await_und_fetch/musterloesung.js create mode 100644 courses/javascript/06_async_await_und_fetch/test_aufgabe.js create mode 100644 courses/javascript/07_einstieg_typescript/README.md create mode 100644 courses/javascript/07_einstieg_typescript/aufgabe.js create mode 100644 courses/javascript/07_einstieg_typescript/index.html create mode 100644 courses/javascript/07_einstieg_typescript/musterloesung.js create mode 100644 courses/javascript/07_einstieg_typescript/test_aufgabe.js create mode 100644 courses/javascript/08_abschlussprojekt_web_app/README.md create mode 100644 courses/javascript/08_abschlussprojekt_web_app/app.js create mode 100644 courses/javascript/08_abschlussprojekt_web_app/aufgabe.js create mode 100644 courses/javascript/08_abschlussprojekt_web_app/index.html create mode 100644 courses/javascript/08_abschlussprojekt_web_app/musterloesung.js create mode 100644 courses/javascript/08_abschlussprojekt_web_app/test_aufgabe.js 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); -
+
💻 In Web-IDE öffnen → + Weiter zu JS 02 →
diff --git a/courses/javascript/02_funktionen_und_arrays/index.html b/courses/javascript/02_funktionen_und_arrays/index.html index 1e9f220..53ed4ff 100644 --- a/courses/javascript/02_funktionen_und_arrays/index.html +++ b/courses/javascript/02_funktionen_und_arrays/index.html @@ -42,8 +42,10 @@ const summe = zahlen.reduce((a, b) => a + b, 0);
-
+
💻 In Web-IDE öffnen → + ← Zurück zu JS 01 + Weiter zu JS 03 →
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 + + + +
+
+
+
+
🌐 JavaScript Grundlagen
+

JS 03: Objekte, Destructuring & Rest/Spread

+

Lerne strukturierte Datenmodellierung mit Object Literals, ES6-Destructuring und dem Rest/Spread-Operator.

+
+
+ +
+ +
+

💡 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 / MethodeBeispielBeschreibung
{ a, b }const obj = { name, email };Property Shorthand
const { k } = objconst { 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
+
+
+ + +
+

🎯 Aufgaben & Checkliste in aufgabe.js

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
💡 Tipp für Teilziel 5
+

Du kannst Destructuring direkt in der Funktionssignatur von reduce oder im Schleifenkopf nutzen: + items.reduce((sum, { preis = 0, anzahl = 1 }) => sum + (preis * anzahl), 0)

+
+ + +
+
+
+
+ + 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 + │ + + ┌───┴───┐ + + │ ┌───┴────────┐ + <h1> <div> (Container) + ┌───┴───┐ + <button> <ul> + │ + <li> +``` + +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 `<li>`-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 <li>-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 <li>-Element +function erstelleListenEintrag(titel, istErledigt = false) { + // TODO: <li> 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 @@ +<!DOCTYPE html> +<html lang="de"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>JS 04: DOM-Manipulation & Events + + + +
+
+
+
+
🌐 JavaScript DOM & Web APIs
+

JS 04: DOM-Manipulation & Events

+

Lerne dynamische Webseiten zu bauen mit dem Document Object Model (DOM), Event-Listenern und UI-Zustandslogik.

+
+
+ +
+ +
+

🌳 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 / EigenschaftBeispielWirkung
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.textContentel.textContent = "Hallo Welt"Liest oder setzt reinen Text
element.classList.add/remove/containsel.classList.contains("aktiv")Manipuliert und prüft CSS-Klassen
element.addEventListener(ev, fn)el.addEventListener("click", () => ...)Registriert Callback bei Ereignis
+
+
+ + +
+

🎯 Aufgaben & Checkliste in aufgabe.js

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
💡 Architektur-Tipp: Headless-UI & Unittests
+

Indem du DOM-Helfer und Zustandslogik in modulare Funktionen aufteilst, kann dein Code sowohl im Webbrowser als auch in isolierten Testumgebungen (wie Web-Workern und CI-Pipelines) zuverlässig getestet werden.

+
+ + +
+
+
+
+ + 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 + + + + +
    +
    +
    +
    +
    🌐 JavaScript Asynchronität
    +

    JS 05: Callbacks, Event Loop & Promises

    +

    Meistere nicht-blockierenden Code, den Event Loop und moderne Promises für robuste Web-Anwendungen.

    +
    +
    + +
    + +
    +

    ⚡ 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));
    +
    + + +
    +

    🎯 Aufgaben & Checkliste

    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    + + +
    + 💡 Lösungstipps & Hilfestellung +
      +
    • Nutze return new Promise((resolve, reject) => { ... });.
    • +
    • Innerhalb des Executors rufst du resolve(wert) bei Erfolg oder reject(new Error("...")) bei Fehlern auf.
    • +
    • Promise.all nimmt ein Array entgegen: Promise.all([quelleA, quelleB]).
    • +
    +
    + + +
    +
    +
    +
    + + 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) + + + + +
    +
    +
    +
    +
    🌐 Modernes JavaScript
    +

    JS 06: Async/Await & REST-APIs (Fetch)

    +

    Schreibe asynchronen Code so lesbar und linear wie synchronen Code mit async, await und try/catch.

    +
    +
    + +
    + +
    +

    📖 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(); +

    +
    +
    +
    + + +
    +

    🎯 Aufgaben & Checkliste (Subgoals)

    +
    + + +
    +
    + + +
    +
    + + +
    + + +
    + 💡 Lösungstipps & Hilfestellung +
      +
    • Deklariere jede asynchrone Funktion mit async function ....
    • +
    • Prüfe vor await res.json() immer mit if (!res.ok) auf HTTP-Fehler.
    • +
    • Im try / catch-Block fängst du fehlgeschlagene Promises sauber ab und lieferst den Fallback-Wert.
    • +
    +
    + + +
    +
    +
    +
    + + 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 + + + +
    +
    +
    +
    +
    🛡️ TypeScript & Type Safety
    +

    TS 07: TypeScript Type Safety & Interfaces

    +

    Entdecke die Macht statischer Typen: Interfaces, Union Types, Generics und robuster Code ohne Laufzeit-Überraschungen.

    +
    +
    + +
    + + + + +
    +

    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:

    + +
    +
    +
    + + + +
    + TypeScript (Basis-Typen & Type Aliases) +
    +
    // 1. Primitive Typen
    +const name: string = "Max Mustermann";
    +const alter: number = 34;
    +const aktiv: boolean = true;
    +const hobbies: string[] = ["Coding", "Design"];
    +
    +// 2. Type Aliases & Union Types (|)
    +type ID = string | number;
    +type Status = "offen" | "in_bearbeitung" | "abgeschlossen" | "storniert";
    +
    +// 3. Spezialtypen
    +let beliebig: any = "Gefährlich: Deaktiviert alle Checks";
    +let sicherer: unknown = JSON.parse(daten); // Erfordert Prüfung vor Zugriff
    +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[]
    +
    + + +
    +

    5. 🎯 Aufgaben & Checkliste

    +

    Öffne die Datei aufgabe.js und setze folgende 4 Teilziele um:

    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + + +
    + + + +
    +
    +
    + + + + 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

    +
    + +
    + + +
    +
    +
    Gesamt
    +
    0
    +
    +
    +
    Aktiv / Offen
    +
    0
    +
    +
    +
    Erledigt
    +
    0
    +
    +
    +
    🔥 Hohe Prio
    +
    0
    +
    +
    +
    + + +
    +
    +
    + + + + +
    +
    +
    + + +
    +
    + + + +
    + +
    + +
    +
    + + +
    + +
    +
    + `; + } + + 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) + + + +
    +
    +
    +
    +
    🏆 Master-Abschlussprojekt
    +

    Master 08: Interaktive Task-App (MVC)

    +

    Kombiniere TypeScript-Typsicherheit, State-Management und reaktive DOM-Manipulation zu einer professionellen Web-Applikation.

    +
    +
    + +
    + + + + +
    +

    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:

    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + + +
    + + + +
    +
    +
    + + + + + + + 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" } ] } ]