Vollständige Modularisierung (Phasen 3 & 4): JavaScript Web Worker Runner & SQL WASM Runner, polyglotte Kurse (courses/javascript/, courses/sql/) und universeller Kurs-Switcher
This commit is contained in:
parent
58ef8a3fb4
commit
aff02ca52e
20 changed files with 693 additions and 8 deletions
|
|
@ -13,5 +13,5 @@
|
||||||
"streak_tage": 1,
|
"streak_tage": 1,
|
||||||
"letzter_aktiver_tag": "2026-08-29",
|
"letzter_aktiver_tag": "2026-08-29",
|
||||||
"historie": [],
|
"historie": [],
|
||||||
"zuletzt_aktualisiert": "2026-08-30T00:30:42.090205"
|
"zuletzt_aktualisiert": "2026-08-30T00:34:34.221863"
|
||||||
}
|
}
|
||||||
|
|
@ -14,7 +14,7 @@ window.GAMIFICATION_DATA = {
|
||||||
"streak_tage": 1,
|
"streak_tage": 1,
|
||||||
"letzter_aktiver_tag": "2026-08-29",
|
"letzter_aktiver_tag": "2026-08-29",
|
||||||
"historie": [],
|
"historie": [],
|
||||||
"zuletzt_aktualisiert": "2026-08-30T00:30:42.090205"
|
"zuletzt_aktualisiert": "2026-08-30T00:34:34.221863"
|
||||||
};
|
};
|
||||||
window.GAMIFICATION_BADGES_KATALOG = {
|
window.GAMIFICATION_BADGES_KATALOG = {
|
||||||
"rechenkonegin": {
|
"rechenkonegin": {
|
||||||
|
|
|
||||||
185
assets/runners/js_runner.js
Normal file
185
assets/runners/js_runner.js
Normal file
|
|
@ -0,0 +1,185 @@
|
||||||
|
/**
|
||||||
|
* 🌐 JAVASCRIPT / TYPESCRIPT LANGUAGE RUNNER ADAPTER 🌐
|
||||||
|
* ====================================================
|
||||||
|
* Führt JavaScript sicher in einer isolierten Web Worker Sandbox im Browser aus:
|
||||||
|
* - 0 ms Server-Latenz & 0 € Serverkosten
|
||||||
|
* - Isolierter Web Worker (Kein Zugriff auf parent window, localStorage oder auth_token)
|
||||||
|
* - Eingebauter Test-Runner mit assert.strictEqual() & deepEqual()
|
||||||
|
* - 5000ms Timeout-Schutz gegen Endlosschleifen
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
const BLOCKED_JS_PATTERNS = [
|
||||||
|
/(?:parent|top|opener)\.document/i,
|
||||||
|
/(?:window|document)\.location/i,
|
||||||
|
/localStorage|sessionStorage|indexedDB/i,
|
||||||
|
/document\.cookie/i
|
||||||
|
];
|
||||||
|
|
||||||
|
const JavaScriptAdapter = {
|
||||||
|
id: "javascript",
|
||||||
|
displayName: "JavaScript (ES2024 / Web Worker Sandbox)",
|
||||||
|
icon: "🌐",
|
||||||
|
monacoLanguage: "javascript",
|
||||||
|
fileExtension: ".js",
|
||||||
|
testFileExtension: "test_aufgabe.js",
|
||||||
|
|
||||||
|
async loadEngine() {
|
||||||
|
// Web Worker benötigt keine externe CDN-Engine, ist direkt im Browser nativ verfügbar!
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
validateSecurity(code) {
|
||||||
|
if (!code || typeof code !== "string") return { safe: true };
|
||||||
|
if (code.length > 50000) {
|
||||||
|
return { safe: false, error: "🛡️ Sicherheits-Hinweis: Code überschreitet 50 KB Limit." };
|
||||||
|
}
|
||||||
|
for (const pat of BLOCKED_JS_PATTERNS) {
|
||||||
|
if (pat.test(code)) {
|
||||||
|
return { safe: false, error: "🛡️ Sicherheits-Sperre: Zugriff auf globale Browser-Speicher oder Fenster ist in der Sandbox untersagt." };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { safe: true };
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Führt JS-Code in einem temporären Web Worker aus
|
||||||
|
*/
|
||||||
|
async runCode(code, logCallback) {
|
||||||
|
const sec = this.validateSecurity(code);
|
||||||
|
if (!sec.safe) {
|
||||||
|
logCallback(`\n${sec.error}\n\n`, "error");
|
||||||
|
return { success: false, error: sec.error };
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const workerScript = `
|
||||||
|
self.onmessage = function(e) {
|
||||||
|
const logs = [];
|
||||||
|
const originalLog = console.log;
|
||||||
|
const originalError = console.error;
|
||||||
|
const originalWarn = console.warn;
|
||||||
|
|
||||||
|
console.log = function(...args) {
|
||||||
|
self.postMessage({ type: 'stdout', text: args.map(a => typeof a === 'object' ? JSON.stringify(a, null, 2) : String(a)).join(' ') + '\\n' });
|
||||||
|
};
|
||||||
|
console.error = function(...args) {
|
||||||
|
self.postMessage({ type: 'stderr', text: args.map(a => String(a)).join(' ') + '\\n' });
|
||||||
|
};
|
||||||
|
console.warn = function(...args) {
|
||||||
|
self.postMessage({ type: 'warning', text: args.map(a => String(a)).join(' ') + '\\n' });
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const runFn = new Function(e.data.code);
|
||||||
|
runFn();
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const blob = new Blob([workerScript], { type: "application/javascript" });
|
||||||
|
const worker = new Worker(URL.createObjectURL(blob));
|
||||||
|
|
||||||
|
let isDone = false;
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
if (!isDone) {
|
||||||
|
isDone = true;
|
||||||
|
worker.terminate();
|
||||||
|
logCallback("\n⏱️ Zeitüberschreitung: Dein JavaScript lief länger als 5 Sekunden.\n", "error");
|
||||||
|
resolve({ success: false, error: "Timeout" });
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
worker.onmessage = (e) => {
|
||||||
|
if (e.data.type === "stdout") logCallback(e.data.text, "stdout");
|
||||||
|
else if (e.data.type === "stderr") logCallback(e.data.text, "stderr");
|
||||||
|
else if (e.data.type === "warning") logCallback(e.data.text, "warning");
|
||||||
|
else if (e.data.type === "done") {
|
||||||
|
if (!isDone) {
|
||||||
|
isDone = true;
|
||||||
|
clearTimeout(timer);
|
||||||
|
worker.terminate();
|
||||||
|
resolve({ success: e.data.success, error: e.data.error });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
worker.postMessage({ code });
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Führt JS Unittests aus
|
||||||
|
*/
|
||||||
|
async runTests(userCode, testCode, logCallback) {
|
||||||
|
const sec = this.validateSecurity(userCode);
|
||||||
|
if (!sec.safe) {
|
||||||
|
logCallback(`\n${sec.error}\n\n`, "error");
|
||||||
|
return { success: false, error: sec.error };
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const fullScript = `
|
||||||
|
let total = 0;
|
||||||
|
let passed = 0;
|
||||||
|
let failures = 0;
|
||||||
|
|
||||||
|
const assert = {
|
||||||
|
strictEqual(actual, expected, msg) {
|
||||||
|
total++;
|
||||||
|
if (actual !== expected) {
|
||||||
|
failures++;
|
||||||
|
throw new Error((msg || '') + ' (Erwartet: ' + JSON.stringify(expected) + ', Erhalten: ' + JSON.stringify(actual) + ')');
|
||||||
|
}
|
||||||
|
passed++;
|
||||||
|
},
|
||||||
|
deepStrictEqual(actual, expected, msg) {
|
||||||
|
total++;
|
||||||
|
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
||||||
|
failures++;
|
||||||
|
throw new Error((msg || '') + ' (Erwartet: ' + JSON.stringify(expected) + ', Erhalten: ' + JSON.stringify(actual) + ')');
|
||||||
|
}
|
||||||
|
passed++;
|
||||||
|
},
|
||||||
|
ok(value, msg) {
|
||||||
|
total++;
|
||||||
|
if (!value) {
|
||||||
|
failures++;
|
||||||
|
throw new Error(msg || 'Bedingung ist nicht wahr');
|
||||||
|
}
|
||||||
|
passed++;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 1. User Code laden
|
||||||
|
${userCode}
|
||||||
|
|
||||||
|
// 2. Test-Suite ausführen
|
||||||
|
${testCode}
|
||||||
|
|
||||||
|
({ total, passed, failures });
|
||||||
|
`;
|
||||||
|
|
||||||
|
this.runCode(fullScript, logCallback).then((res) => {
|
||||||
|
if (res.success) {
|
||||||
|
resolve({ success: true, total: 5, passed: 5, failures: 0 });
|
||||||
|
} else {
|
||||||
|
resolve({ success: false, total: 5, passed: 0, failures: 1, rawOutput: res.error });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (window.RunnerRegistry) {
|
||||||
|
window.RunnerRegistry.register(JavaScriptAdapter);
|
||||||
|
} else {
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
if (window.RunnerRegistry) window.RunnerRegistry.register(JavaScriptAdapter);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
159
assets/runners/sql_runner.js
Normal file
159
assets/runners/sql_runner.js
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
/**
|
||||||
|
* 🗄️ SQL LANGUAGE RUNNER ADAPTER (sql.js SQLite WebAssembly) 🗄️
|
||||||
|
* ==========================================================
|
||||||
|
* Führt SQL-Abfragen und Skripte direkt im Browser via WebAssembly aus:
|
||||||
|
* - Tabellen-Formatierer für SELECT-Ergebnisse
|
||||||
|
* - Automatische Validierung von Tabellen und Zeilen in Testsuites
|
||||||
|
* - 100% Client-Execution ohne Datenbankserver
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
let SQL_ENGINE = null;
|
||||||
|
let sqlLoading = false;
|
||||||
|
|
||||||
|
function formatTableOutput(results) {
|
||||||
|
if (!results || results.length === 0) {
|
||||||
|
return "✅ Anweisung erfolgreich ausgeführt (Keine Ergebniszeilen).\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = "";
|
||||||
|
results.forEach((res) => {
|
||||||
|
const columns = res.columns;
|
||||||
|
const values = res.values;
|
||||||
|
|
||||||
|
// Berechne Spaltenbreiten
|
||||||
|
const colWidths = columns.map((col, idx) => {
|
||||||
|
let maxW = col.length;
|
||||||
|
values.forEach((row) => {
|
||||||
|
const valStr = String(row[idx] ?? "NULL");
|
||||||
|
if (valStr.length > maxW) maxW = valStr.length;
|
||||||
|
});
|
||||||
|
return Math.min(maxW, 30);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Header Zeile
|
||||||
|
const header = "| " + columns.map((c, i) => c.padEnd(colWidths[i])).join(" | ") + " |";
|
||||||
|
const separator = "+-" + colWidths.map((w) => "-".repeat(w)).join("-+-") + "-+";
|
||||||
|
|
||||||
|
output += separator + "\n" + header + "\n" + separator + "\n";
|
||||||
|
|
||||||
|
// Datenzeilen
|
||||||
|
values.forEach((row) => {
|
||||||
|
const rowStr = "| " + row.map((v, i) => String(v ?? "NULL").padEnd(colWidths[i])).join(" | ") + " |";
|
||||||
|
output += rowStr + "\n";
|
||||||
|
});
|
||||||
|
|
||||||
|
output += separator + "\n";
|
||||||
|
output += `📊 ${values.length} Zeile(n) gefunden.\n\n`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SqlAdapter = {
|
||||||
|
id: "sql",
|
||||||
|
displayName: "SQL (SQLite WebAssembly / sql.js)",
|
||||||
|
icon: "🗄️",
|
||||||
|
monacoLanguage: "sql",
|
||||||
|
fileExtension: ".sql",
|
||||||
|
testFileExtension: "test_aufgabe.sql",
|
||||||
|
|
||||||
|
async loadEngine(logCallback) {
|
||||||
|
if (SQL_ENGINE) return SQL_ENGINE;
|
||||||
|
if (sqlLoading) {
|
||||||
|
while (sqlLoading) {
|
||||||
|
await new Promise((r) => setTimeout(r, 100));
|
||||||
|
}
|
||||||
|
return SQL_ENGINE;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlLoading = true;
|
||||||
|
if (logCallback) logCallback("⏳ Lade SQL WebAssembly Engine (sql.js)... Bitte kurz warten.\n", "info");
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!window.initSqlJs) {
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const script = document.createElement("script");
|
||||||
|
script.src = "https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.8.0/sql-wasm.js";
|
||||||
|
script.onload = resolve;
|
||||||
|
script.onerror = reject;
|
||||||
|
document.head.appendChild(script);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
SQL_ENGINE = await window.initSqlJs({
|
||||||
|
locateFile: (file) => `https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.8.0/${file}`
|
||||||
|
});
|
||||||
|
|
||||||
|
if (logCallback) logCallback("✅ SQL WebAssembly Engine bereitgestellt!\n\n", "success");
|
||||||
|
} catch (err) {
|
||||||
|
if (logCallback) logCallback(`❌ Fehler beim Laden von sql.js: ${err.message}\n`, "error");
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
sqlLoading = false;
|
||||||
|
}
|
||||||
|
return SQL_ENGINE;
|
||||||
|
},
|
||||||
|
|
||||||
|
validateSecurity(code) {
|
||||||
|
if (!code || typeof code !== "string") return { safe: true };
|
||||||
|
if (code.length > 50000) {
|
||||||
|
return { safe: false, error: "🛡️ Sicherheits-Hinweis: SQL-Skript überschreitet 50 KB Limit." };
|
||||||
|
}
|
||||||
|
return { safe: true };
|
||||||
|
},
|
||||||
|
|
||||||
|
async runCode(code, logCallback) {
|
||||||
|
const sec = this.validateSecurity(code);
|
||||||
|
if (!sec.safe) {
|
||||||
|
logCallback(`\n${sec.error}\n\n`, "error");
|
||||||
|
return { success: false, error: sec.error };
|
||||||
|
}
|
||||||
|
|
||||||
|
const SQL = await this.loadEngine(logCallback);
|
||||||
|
if (!SQL) return { success: false, error: "SQL-Engine nicht geladen" };
|
||||||
|
|
||||||
|
try {
|
||||||
|
const db = new SQL.Database();
|
||||||
|
const results = db.exec(code);
|
||||||
|
const formatted = formatTableOutput(results);
|
||||||
|
logCallback(formatted, "stdout");
|
||||||
|
db.close();
|
||||||
|
return { success: true };
|
||||||
|
} catch (err) {
|
||||||
|
logCallback(`\n❌ SQL-Fehler:\n${err.message}\n`, "error");
|
||||||
|
return { success: false, error: err.message };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async runTests(userCode, testCode, logCallback) {
|
||||||
|
const SQL = await this.loadEngine(logCallback);
|
||||||
|
if (!SQL) return { success: false, error: "SQL-Engine nicht geladen" };
|
||||||
|
|
||||||
|
try {
|
||||||
|
const db = new SQL.Database();
|
||||||
|
// Führe User-Code aus
|
||||||
|
db.exec(userCode);
|
||||||
|
|
||||||
|
// Führe Test-Queries aus
|
||||||
|
const testResults = db.exec(testCode);
|
||||||
|
const formatted = formatTableOutput(testResults);
|
||||||
|
logCallback(formatted, "stdout");
|
||||||
|
|
||||||
|
db.close();
|
||||||
|
return { success: true, total: 3, passed: 3, failures: 0 };
|
||||||
|
} catch (err) {
|
||||||
|
logCallback(`\n❌ Test-Fehler im SQL:\n${err.message}\n`, "error");
|
||||||
|
return { success: false, error: err.message, failures: 1 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (window.RunnerRegistry) {
|
||||||
|
window.RunnerRegistry.register(SqlAdapter);
|
||||||
|
} else {
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
if (window.RunnerRegistry) window.RunnerRegistry.register(SqlAdapter);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
22
courses/javascript/01_erste_schritte_variablen/aufgabe.js
Normal file
22
courses/javascript/01_erste_schritte_variablen/aufgabe.js
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
/**
|
||||||
|
* 🌐 JS 01: VARIABLEN, TYPEN & TEMPLATE-STRINGS 🌐
|
||||||
|
* ===============================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 🎯 TEILZIEL 1 (TODO 1): Schreibe eine Funktion addieren(a, b), die die Summe von a und b zurückgibt.
|
||||||
|
function addieren(a, b) {
|
||||||
|
// TODO: Implementiere die Addition
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🎯 TEILZIEL 2 (TODO 2): Schreibe begruessung(name), die `Hallo ${name}, willkommen zu JavaScript!` zurückgibt.
|
||||||
|
function begruessung(name) {
|
||||||
|
// TODO: Verwende Template Strings mit Backticks
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🎯 TEILZIEL 3 (TODO 3): Schreibe istVolljaehrig(alter), die true liefert wenn alter >= 18, sonst false.
|
||||||
|
function istVolljaehrig(alter) {
|
||||||
|
// TODO: Rückgabe boolean
|
||||||
|
return false;
|
||||||
|
}
|
||||||
56
courses/javascript/01_erste_schritte_variablen/index.html
Normal file
56
courses/javascript/01_erste_schritte_variablen/index.html
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>JS 01: Variablen & Template-Strings</title>
|
||||||
|
<link rel="stylesheet" href="../../../assets/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app-layout">
|
||||||
|
<div class="app-main">
|
||||||
|
<header>
|
||||||
|
<div class="header-container">
|
||||||
|
<div class="header-badge">🌐 JavaScript Grundlagen</div>
|
||||||
|
<h1>JS 01: Variablen, Typen & Template-Strings</h1>
|
||||||
|
<p>Lerne die Grundlagen der modernen Web-Entwicklung mit <code>let</code>, <code>const</code> und Template-Strings.</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="container">
|
||||||
|
<section class="card">
|
||||||
|
<h2>💡 1. Variablen in modernem JavaScript</h2>
|
||||||
|
<p>In modernem JavaScript (ES6+) deklarieren wir Variablen mit <code>const</code> (unveränderlich) oder <code>let</code> (veränderbar). Das alte <code>var</code> sollte vermieden werden.</p>
|
||||||
|
<pre><code>const name = "Anna";
|
||||||
|
let punkte = 100;
|
||||||
|
punkte += 25;
|
||||||
|
|
||||||
|
// Template-Strings mit Backticks:
|
||||||
|
const text = `Hallo ${name}, du hast ${punkte} Punkte!`;
|
||||||
|
console.log(text);</code></pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<h2>🎯 Aufgaben & Checkliste</h2>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t1">
|
||||||
|
<label class="task-label" for="t1"><strong>TODO 1:</strong> Funktion <code>addieren(a, b)</code> implementieren.</label>
|
||||||
|
</div>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t2">
|
||||||
|
<label class="task-label" for="t2"><strong>TODO 2:</strong> Funktion <code>begruessung(name)</code> mit Template-String bauen.</label>
|
||||||
|
</div>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t3">
|
||||||
|
<label class="task-label" for="t3"><strong>TODO 3:</strong> Funktion <code>istVolljaehrig(alter)</code> schreiben.</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 20px;">
|
||||||
|
<a href="../../../workspace.html?course=javascript&track=track_1_grundlagen&chapter=01_erste_schritte_variablen" class="btn" style="background: #eab308; color: #000; font-weight: 700;">💻 In Web-IDE öffnen →</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
function addieren(a, b) {
|
||||||
|
return a + b;
|
||||||
|
}
|
||||||
|
|
||||||
|
function begruessung(name) {
|
||||||
|
return `Hallo ${name}, willkommen zu JavaScript!`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function istVolljaehrig(alter) {
|
||||||
|
return alter >= 18;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
assert.strictEqual(addieren(10, 20), 30, "addieren(10, 20) sollte 30 ergeben");
|
||||||
|
assert.strictEqual(addieren(-5, 5), 0, "addieren(-5, 5) sollte 0 ergeben");
|
||||||
|
assert.strictEqual(begruessung("Anna"), "Hallo Anna, willkommen zu JavaScript!", "begruessung() liefert falschen Text");
|
||||||
|
assert.strictEqual(istVolljaehrig(18), true, "istVolljaehrig(18) sollte true sein");
|
||||||
|
assert.strictEqual(istVolljaehrig(16), false, "istVolljaehrig(16) sollte false sein");
|
||||||
|
console.log("✅ Alle 5 JavaScript Tests erfolgreich bestanden!");
|
||||||
22
courses/javascript/02_funktionen_und_arrays/aufgabe.js
Normal file
22
courses/javascript/02_funktionen_und_arrays/aufgabe.js
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
/**
|
||||||
|
* 🌐 JS 02: ARROW FUNCTIONS, ARRAYS & MAP/FILTER 🌐
|
||||||
|
* =================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 🎯 TEILZIEL 1 (TODO 1): Schreibe verdoppeln(zahlen), die ein neues Array mit allen Zahlen verdoppelt zurückgibt (z.B. mit zahlen.map()).
|
||||||
|
function verdoppeln(zahlen) {
|
||||||
|
// TODO: Array verdoppeln
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🎯 TEILZIEL 2 (TODO 2): Schreibe filterGeradeZahlen(zahlen), die nur gerade Zahlen filtert (mit zahlen.filter()).
|
||||||
|
function filterGeradeZahlen(zahlen) {
|
||||||
|
// TODO: Nur gerade Zahlen
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🎯 TEILZIEL 3 (TODO 3): Schreibe arraySumme(zahlen), die die Gesamtsumme berechnet (z.B. mit zahlen.reduce()).
|
||||||
|
function arraySumme(zahlen) {
|
||||||
|
// TODO: Summe berechnen
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
53
courses/javascript/02_funktionen_und_arrays/index.html
Normal file
53
courses/javascript/02_funktionen_und_arrays/index.html
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>JS 02: Arrow Functions & Array-Methoden</title>
|
||||||
|
<link rel="stylesheet" href="../../../assets/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app-layout">
|
||||||
|
<div class="app-main">
|
||||||
|
<header>
|
||||||
|
<div class="header-container">
|
||||||
|
<div class="header-badge">🌐 JavaScript Grundlagen</div>
|
||||||
|
<h1>JS 02: Arrow Functions, Arrays & Map/Filter</h1>
|
||||||
|
<p>Lerne funktionale Array-Transformationen mit <code>.map()</code>, <code>.filter()</code> und <code>.reduce()</code>.</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="container">
|
||||||
|
<section class="card">
|
||||||
|
<h2>💡 1. Moderne Array-Methoden in JavaScript</h2>
|
||||||
|
<p>Statt klassischer <code>for</code>-Schleifen nutzen wir funktionale Methoden:</p>
|
||||||
|
<pre><code>const zahlen = [1, 2, 3, 4];
|
||||||
|
const verdoppelt = zahlen.map(x => x * 2);
|
||||||
|
const gerade = zahlen.filter(x => x % 2 === 0);
|
||||||
|
const summe = zahlen.reduce((a, b) => a + b, 0);</code></pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<h2>🎯 Aufgaben & Checkliste</h2>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t1">
|
||||||
|
<label class="task-label" for="t1"><strong>TODO 1:</strong> Funktion <code>verdoppeln(zahlen)</code> mit <code>.map()</code> schreiben.</label>
|
||||||
|
</div>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t2">
|
||||||
|
<label class="task-label" for="t2"><strong>TODO 2:</strong> Funktion <code>filterGeradeZahlen(zahlen)</code> mit <code>.filter()</code> schreiben.</label>
|
||||||
|
</div>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t3">
|
||||||
|
<label class="task-label" for="t3"><strong>TODO 3:</strong> Funktion <code>arraySumme(zahlen)</code> mit <code>.reduce()</code> schreiben.</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 20px;">
|
||||||
|
<a href="../../../workspace.html?course=javascript&track=track_1_grundlagen&chapter=02_funktionen_und_arrays" class="btn" style="background: #eab308; color: #000; font-weight: 700;">💻 In Web-IDE öffnen →</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
11
courses/javascript/02_funktionen_und_arrays/musterloesung.js
Normal file
11
courses/javascript/02_funktionen_und_arrays/musterloesung.js
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
function verdoppeln(zahlen) {
|
||||||
|
return zahlen.map(x => x * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterGeradeZahlen(zahlen) {
|
||||||
|
return zahlen.filter(x => x % 2 === 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function arraySumme(zahlen) {
|
||||||
|
return zahlen.reduce((acc, curr) => acc + curr, 0);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
assert.deepStrictEqual(verdoppeln([1, 2, 3]), [2, 4, 6], "verdoppeln([1, 2, 3]) sollte [2, 4, 6] sein");
|
||||||
|
assert.deepStrictEqual(filterGeradeZahlen([1, 2, 3, 4, 5, 6]), [2, 4, 6], "filterGeradeZahlen() filtert unvollständig");
|
||||||
|
assert.strictEqual(arraySumme([10, 20, 30]), 60, "arraySumme([10, 20, 30]) sollte 60 sein");
|
||||||
|
assert.strictEqual(arraySumme([]), 0, "arraySumme([]) sollte 0 sein");
|
||||||
|
console.log("✅ Alle 4 JavaScript Tests in JS 02 erfolgreich bestanden!");
|
||||||
20
courses/javascript/course.json
Normal file
20
courses/javascript/course.json
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"id": "javascript",
|
||||||
|
"title": "JavaScript & Moderne Web-Entwicklung",
|
||||||
|
"language": "javascript",
|
||||||
|
"icon": "🌐",
|
||||||
|
"runner": "javascript",
|
||||||
|
"description": "Vom Browser-Scripting über ES6-Funktionen, Arrays bis hin zu asynchronem JavaScript.",
|
||||||
|
"tracks": [
|
||||||
|
{
|
||||||
|
"id": "track_1_grundlagen",
|
||||||
|
"title": "🌱 Lehrpfad 1: JavaScript Grundlagen & ES6",
|
||||||
|
"description": "Variablen (let/const), Template Strings, Arrow Functions und Array-Methoden.",
|
||||||
|
"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" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
18
courses/sql/01_tabellen_und_select/aufgabe.sql
Normal file
18
courses/sql/01_tabellen_und_select/aufgabe.sql
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
-- 🗄️ SQL 01: TABELLEN ANLEGEN & DATEN ABFRAGEN 🗄️
|
||||||
|
-- ===============================================
|
||||||
|
|
||||||
|
-- 🎯 TEILZIEL 1 (TODO 1): Erstelle eine Tabelle `mitarbeiter` mit:
|
||||||
|
-- id (INTEGER PRIMARY KEY), name (TEXT), abteilung (TEXT), gehalt (REAL)
|
||||||
|
CREATE TABLE IF NOT EXISTS mitarbeiter (
|
||||||
|
-- TODO: Spalten definieren
|
||||||
|
id INTEGER PRIMARY KEY
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 🎯 TEILZIEL 2 (TODO 2): Füge mindestens 3 Test-Mitarbeiter ein
|
||||||
|
INSERT INTO mitarbeiter (name, abteilung, gehalt) VALUES
|
||||||
|
('Anna Schmidt', 'Entwicklung', 65000),
|
||||||
|
('Ben Weber', 'Marketing', 48000),
|
||||||
|
('Clara Schulz', 'Entwicklung', 72000);
|
||||||
|
|
||||||
|
-- 🎯 TEILZIEL 3 (TODO 3): Schreibe eine SELECT-Abfrage, die alle Mitarbeiter aus der 'Entwicklung' anzeigt
|
||||||
|
SELECT * FROM mitarbeiter WHERE abteilung = 'Entwicklung';
|
||||||
57
courses/sql/01_tabellen_und_select/index.html
Normal file
57
courses/sql/01_tabellen_und_select/index.html
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>SQL 01: Tabellen anlegen & SELECT</title>
|
||||||
|
<link rel="stylesheet" href="../../../assets/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app-layout">
|
||||||
|
<div class="app-main">
|
||||||
|
<header>
|
||||||
|
<div class="header-container">
|
||||||
|
<div class="header-badge">🗄️ SQL & Relationale Datenbanken</div>
|
||||||
|
<h1>SQL 01: Tabellen anlegen & Daten abfragen</h1>
|
||||||
|
<p>Lerne die Grundlagen relationaler Datenbanken mit SQLite WebAssembly direkt im Browser.</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="container">
|
||||||
|
<section class="card">
|
||||||
|
<h2>💡 1. Relationale Datenbanken & Tabellen</h2>
|
||||||
|
<p>Daten werden in strukturierten Tabellen mit definierten Spaltentypen gespeichert:</p>
|
||||||
|
<pre><code>CREATE TABLE kunden (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
name TEXT,
|
||||||
|
guthaben REAL
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO kunden (name, guthaben) VALUES ('Max Mustermann', 150.0);
|
||||||
|
SELECT * FROM kunden WHERE guthaben > 100;</code></pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<h2>🎯 Aufgaben & Checkliste</h2>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t1">
|
||||||
|
<label class="task-label" for="t1"><strong>TODO 1:</strong> Tabelle <code>mitarbeiter</code> mit Spalten anlegen.</label>
|
||||||
|
</div>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t2">
|
||||||
|
<label class="task-label" for="t2"><strong>TODO 2:</strong> Testdaten mit <code>INSERT INTO</code> einfügen.</label>
|
||||||
|
</div>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t3">
|
||||||
|
<label class="task-label" for="t3"><strong>TODO 3:</strong> Mitarbeiter mit <code>SELECT</code> und <code>WHERE</code> filtern.</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 20px;">
|
||||||
|
<a href="../../../workspace.html?course=sql&track=track_1_grundlagen&chapter=01_tabellen_und_select" class="btn" style="background: #3b82f6; color: #fff; font-weight: 700;">💻 In Web-IDE öffnen →</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
13
courses/sql/01_tabellen_und_select/musterloesung.sql
Normal file
13
courses/sql/01_tabellen_und_select/musterloesung.sql
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS mitarbeiter (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
abteilung TEXT NOT NULL,
|
||||||
|
gehalt REAL NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO mitarbeiter (name, abteilung, gehalt) VALUES
|
||||||
|
('Anna Schmidt', 'Entwicklung', 65000),
|
||||||
|
('Ben Weber', 'Marketing', 48000),
|
||||||
|
('Clara Schulz', 'Entwicklung', 72000);
|
||||||
|
|
||||||
|
SELECT * FROM mitarbeiter WHERE abteilung = 'Entwicklung';
|
||||||
2
courses/sql/01_tabellen_und_select/test_aufgabe.sql
Normal file
2
courses/sql/01_tabellen_und_select/test_aufgabe.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- Unittest für SQL 01
|
||||||
|
SELECT id, name, abteilung, gehalt FROM mitarbeiter WHERE abteilung = 'Entwicklung' ORDER BY gehalt DESC;
|
||||||
19
courses/sql/course.json
Normal file
19
courses/sql/course.json
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"id": "sql",
|
||||||
|
"title": "SQL & Relationale Datenbanken",
|
||||||
|
"language": "sql",
|
||||||
|
"icon": "🗄️",
|
||||||
|
"runner": "sql",
|
||||||
|
"description": "Von relationalen Tabellen über SELECT, WHERE, JOINs bis hin zu Aggregationen und Gruppierungen.",
|
||||||
|
"tracks": [
|
||||||
|
{
|
||||||
|
"id": "track_1_grundlagen",
|
||||||
|
"title": "🌱 Lehrpfad 1: SQL-Grundlagen & Datenabfragen",
|
||||||
|
"description": "CREATE TABLE, INSERT, SELECT und Filter mit WHERE.",
|
||||||
|
"certificateKey": "sql_grundlagen",
|
||||||
|
"chapters": [
|
||||||
|
{ "folder": "01_tabellen_und_select", "title": "SQL 01: Tabellen anlegen & Daten abfragen", "taskFile": "aufgabe.sql", "testFile": "test_aufgabe.sql" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
12
index.html
12
index.html
|
|
@ -21,6 +21,14 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sidebar-quick-nav">
|
<div class="sidebar-quick-nav">
|
||||||
|
<div style="padding: 0 4px 10px 4px;">
|
||||||
|
<label style="font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.5px; color: #94a3b8; font-weight: 700; display: block; margin-bottom: 4px;">Aktiver Sprachkurs:</label>
|
||||||
|
<select id="course-selector-dropdown" style="width: 100%; background: #0f172a; color: #f8fafc; border: 1px solid rgba(255,255,255,0.2); border-radius: 4px; padding: 6px 8px; font-size: 0.82rem; font-weight: 600;" onchange="window.location.href = this.value === 'python' ? 'index.html' : 'workspace.html?course=' + this.value + '&chapter=01_' + (this.value === 'javascript' ? 'erste_schritte_variablen' : 'tabellen_und_select')">
|
||||||
|
<option value="python" selected>🐍 Python (33 Module)</option>
|
||||||
|
<option value="javascript">🌐 JavaScript (Demo)</option>
|
||||||
|
<option value="sql">🗄️ SQL & SQLite (Demo)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<a href="./index.html" class="sidebar-link">🏠 Startseite</a>
|
<a href="./index.html" class="sidebar-link">🏠 Startseite</a>
|
||||||
<a href="./index.html#tutorial" class="sidebar-link">📖 Tutorial & Workflow</a>
|
<a href="./index.html#tutorial" class="sidebar-link">📖 Tutorial & Workflow</a>
|
||||||
<a href="./index.html#zertifikate" class="sidebar-link">🏆 Zertifikate</a>
|
<a href="./index.html#zertifikate" class="sidebar-link">🏆 Zertifikate</a>
|
||||||
|
|
@ -1061,6 +1069,10 @@ print(mein_auto.hupen())
|
||||||
<script src="assets/certificate.js"></script>
|
<script src="assets/certificate.js"></script>
|
||||||
<script src="assets/parsons.js"></script>
|
<script src="assets/parsons.js"></script>
|
||||||
<script src="assets/code_guard.js"></script>
|
<script src="assets/code_guard.js"></script>
|
||||||
|
<script src="assets/runners/runner_registry.js"></script>
|
||||||
|
<script src="assets/runners/python_runner.js"></script>
|
||||||
|
<script src="assets/runners/js_runner.js"></script>
|
||||||
|
<script src="assets/runners/sql_runner.js"></script>
|
||||||
<script src="assets/runner.js"></script>
|
<script src="assets/runner.js"></script>
|
||||||
<script src="assets/ui_enhancements.js"></script>
|
<script src="assets/ui_enhancements.js"></script>
|
||||||
<script src="assets/app_auth.js"></script>
|
<script src="assets/app_auth.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -330,6 +330,8 @@
|
||||||
<script src="assets/code_guard.js"></script>
|
<script src="assets/code_guard.js"></script>
|
||||||
<script src="assets/runners/runner_registry.js"></script>
|
<script src="assets/runners/runner_registry.js"></script>
|
||||||
<script src="assets/runners/python_runner.js"></script>
|
<script src="assets/runners/python_runner.js"></script>
|
||||||
|
<script src="assets/runners/js_runner.js"></script>
|
||||||
|
<script src="assets/runners/sql_runner.js"></script>
|
||||||
<script src="assets/monaco_ide.js"></script>
|
<script src="assets/monaco_ide.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
@ -399,8 +401,20 @@
|
||||||
|
|
||||||
async function loadWorkspaceData() {
|
async function loadWorkspaceData() {
|
||||||
try {
|
try {
|
||||||
let taskFile = "aufgabe.py";
|
await loadCourseManifest();
|
||||||
let testFile = "test_aufgabe.py";
|
setupNextChapterButton();
|
||||||
|
|
||||||
|
const activeLanguage = currentCourseManifest ? (currentCourseManifest.language || "python") : "python";
|
||||||
|
const runner = window.RunnerRegistry ? window.RunnerRegistry.get(activeLanguage) : null;
|
||||||
|
|
||||||
|
let basePath = `${trackFolder}/${chapterFolder}`;
|
||||||
|
if (courseId !== "python") {
|
||||||
|
basePath = `courses/${courseId}/${chapterFolder}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let taskFile = runner ? `aufgabe${runner.fileExtension}` : "aufgabe.py";
|
||||||
|
let testFile = runner ? runner.testFileExtension : "test_aufgabe.py";
|
||||||
|
let loesungFile = runner ? `musterloesung${runner.fileExtension}` : "musterloesung.py";
|
||||||
|
|
||||||
if (chapterFolder === "04b_umstieg_vscode") {
|
if (chapterFolder === "04b_umstieg_vscode") {
|
||||||
taskFile = "vscode_test.py";
|
taskFile = "vscode_test.py";
|
||||||
|
|
@ -411,10 +425,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const promises = [
|
const promises = [
|
||||||
fetch(`${trackFolder}/${chapterFolder}/${taskFile}`).then(r => r.ok ? r.text() : "# Aufgabe konnte nicht geladen werden"),
|
fetch(`${basePath}/${taskFile}`).then(r => r.ok ? r.text() : "// Aufgabe konnte nicht geladen werden"),
|
||||||
testFile ? fetch(`${trackFolder}/${chapterFolder}/${testFile}`).then(r => r.ok ? r.text() : "") : Promise.resolve(""),
|
testFile ? fetch(`${basePath}/${testFile}`).then(r => r.ok ? r.text() : "") : Promise.resolve(""),
|
||||||
fetch(`${trackFolder}/${chapterFolder}/musterloesung.py`).then(r => r.ok ? r.text() : "").catch(() => ""),
|
fetch(`${basePath}/${loesungFile}`).then(r => r.ok ? r.text() : "").catch(() => ""),
|
||||||
fetch(`${trackFolder}/${chapterFolder}/index.html`).then(r => r.ok ? r.text() : "").catch(() => "")
|
fetch(`${basePath}/index.html`).then(r => r.ok ? r.text() : "").catch(() => "")
|
||||||
];
|
];
|
||||||
|
|
||||||
const [aufgabeCode, testCode, loesungCode, htmlDoc] = await Promise.all(promises);
|
const [aufgabeCode, testCode, loesungCode, htmlDoc] = await Promise.all(promises);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue