Implementiere Online-Lernplattform: Monaco Split-Screen Web-IDE, In-Browser Pyodide Test-Runner, User Auth, Auto-Save und Lehrer-Dashboard
This commit is contained in:
parent
0f73a3619f
commit
759e64666a
10 changed files with 2160 additions and 252 deletions
|
|
@ -10,5 +10,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-29T16:00:34.190254"
|
"zuletzt_aktualisiert": "2026-08-29T20:50:51.073181"
|
||||||
}
|
}
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -8,3 +8,5 @@ htmlcov/
|
||||||
venv/
|
venv/
|
||||||
.venv/
|
.venv/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
*.db
|
||||||
|
platform_data.db
|
||||||
|
|
|
||||||
184
api.php
Normal file
184
api.php
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 🐘 PHP BACKEND REST API FÜR STANDARD-WEBSPACE (Hetzner, All-Inkl, Strato, cPanel)
|
||||||
|
* ==============================================================================
|
||||||
|
* SQLite-basierte REST-API für Benutzerverwaltung, Cloud-Speichern & Lehrerklassen.
|
||||||
|
*/
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
header('Access-Control-Allow-Origin: *');
|
||||||
|
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
|
||||||
|
header('Access-Control-Allow-Headers: Content-Type, Authorization');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||||
|
http_response_code(204);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$db_file = __DIR__ . '/platform_data.db';
|
||||||
|
$pdo = new PDO('sqlite:' . $db_file);
|
||||||
|
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||||
|
|
||||||
|
// Init Tabellen
|
||||||
|
$pdo->exec("
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
email TEXT UNIQUE NOT NULL,
|
||||||
|
password_hash TEXT NOT NULL,
|
||||||
|
salt TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
role TEXT DEFAULT 'student',
|
||||||
|
xp INTEGER DEFAULT 0,
|
||||||
|
level INTEGER DEFAULT 1,
|
||||||
|
streak_days INTEGER DEFAULT 1,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS chapter_progress (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
chapter_id TEXT NOT NULL,
|
||||||
|
code_draft TEXT,
|
||||||
|
subgoals_json TEXT,
|
||||||
|
is_solved INTEGER DEFAULT 0,
|
||||||
|
solved_at TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE(user_id, chapter_id)
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS classrooms (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
teacher_id INTEGER NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
invite_code TEXT UNIQUE NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS class_enrollments (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
classroom_id INTEGER NOT NULL,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE(classroom_id, user_id)
|
||||||
|
);
|
||||||
|
");
|
||||||
|
|
||||||
|
$path = $_SERVER['PATH_INFO'] ?? $_GET['action'] ?? '';
|
||||||
|
$body = json_decode(file_get_contents('php://input'), true) ?? [];
|
||||||
|
|
||||||
|
// Helper Auth
|
||||||
|
function get_auth_user($pdo) {
|
||||||
|
$headers = getallheaders();
|
||||||
|
$auth = $headers['Authorization'] ?? $headers['authorization'] ?? '';
|
||||||
|
if (strpos($auth, 'Bearer ') === 0) {
|
||||||
|
$token = substr($auth, 7);
|
||||||
|
$parts = explode('.', $token);
|
||||||
|
if (count($parts) === 2) {
|
||||||
|
$payload = json_decode(base64_decode($parts[0]), true);
|
||||||
|
if ($payload && isset($payload['uid'])) {
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function create_token($uid, $email, $role) {
|
||||||
|
$payload = base64_encode(json_encode(['uid' => $uid, 'email' => $email, 'role' => $role]));
|
||||||
|
$sig = hash_hmac('sha256', $payload, 'python_lernportal_secret');
|
||||||
|
return $payload . '.' . $sig;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. REGISTER
|
||||||
|
if ($path === '/auth/register') {
|
||||||
|
$email = strtolower(trim($body['email'] ?? ''));
|
||||||
|
$password = $body['password'] ?? '';
|
||||||
|
$name = trim($body['name'] ?? '');
|
||||||
|
$role = $body['role'] ?? 'student';
|
||||||
|
|
||||||
|
if (!$email || !$password || !$name) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'Pflichtfelder fehlen']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$salt = bin2hex(random_bytes(16));
|
||||||
|
$hash = hash_pbkdf2('sha256', $password, $salt, 100000);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO users (email, password_hash, salt, name, role) VALUES (?, ?, ?, ?, ?)");
|
||||||
|
$stmt->execute([$email, $hash, $salt, $name, $role]);
|
||||||
|
$uid = $pdo->lastInsertId();
|
||||||
|
$token = create_token($uid, $email, $role);
|
||||||
|
http_response_code(201);
|
||||||
|
echo json_encode(['token' => $token, 'user' => ['id' => $uid, 'email' => $email, 'name' => $name, 'role' => $role, 'xp' => 0, 'level' => 1]]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
http_response_code(409);
|
||||||
|
echo json_encode(['error' => 'E-Mail existiert bereits']);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. LOGIN
|
||||||
|
if ($path === '/auth/login') {
|
||||||
|
$email = strtolower(trim($body['email'] ?? ''));
|
||||||
|
$password = $body['password'] ?? '';
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = ?");
|
||||||
|
$stmt->execute([$email]);
|
||||||
|
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if ($user) {
|
||||||
|
$hash = hash_pbkdf2('sha256', $password, $user['salt'], 100000);
|
||||||
|
if (hash_equals($hash, $user['password_hash'])) {
|
||||||
|
$token = create_token($user['id'], $user['email'], $user['role']);
|
||||||
|
echo json_encode([
|
||||||
|
'token' => $token,
|
||||||
|
'user' => [
|
||||||
|
'id' => $user['id'],
|
||||||
|
'email' => $user['email'],
|
||||||
|
'name' => $user['name'],
|
||||||
|
'role' => $user['role'],
|
||||||
|
'xp' => (int)$user['xp'],
|
||||||
|
'level' => (int)$user['level']
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['error' => 'Ungültige Anmeldedaten']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. PROGRESS SAVE
|
||||||
|
if ($path === '/progress/save') {
|
||||||
|
$user = get_auth_user($pdo);
|
||||||
|
if (!$user) { http_response_code(401); echo json_encode(['error' => 'Nicht angemeldet']); exit; }
|
||||||
|
|
||||||
|
$chapter_id = $body['chapter_id'] ?? '';
|
||||||
|
$code_draft = $body['code_draft'] ?? '';
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO chapter_progress (user_id, chapter_id, code_draft) VALUES (?, ?, ?) ON CONFLICT(user_id, chapter_id) DO UPDATE SET code_draft = excluded.code_draft, updated_at = CURRENT_TIMESTAMP");
|
||||||
|
$stmt->execute([$user['uid'], $chapter_id, $code_draft]);
|
||||||
|
echo json_encode(['status' => 'saved']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. PROGRESS SOLVE
|
||||||
|
if ($path === '/progress/solve') {
|
||||||
|
$user = get_auth_user($pdo);
|
||||||
|
if (!$user) { http_response_code(401); echo json_encode(['error' => 'Nicht angemeldet']); exit; }
|
||||||
|
|
||||||
|
$chapter_id = $body['chapter_id'] ?? '';
|
||||||
|
$xp_reward = (int)($body['xp_reward'] ?? 100);
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO chapter_progress (user_id, chapter_id, is_solved, solved_at) VALUES (?, ?, 1, CURRENT_TIMESTAMP) ON CONFLICT(user_id, chapter_id) DO UPDATE SET is_solved = 1, solved_at = CURRENT_TIMESTAMP");
|
||||||
|
$stmt->execute([$user['uid'], $chapter_id]);
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("UPDATE users SET xp = xp + ?, level = ((xp + ?) / 100) + 1 WHERE id = ?");
|
||||||
|
$stmt->execute([$xp_reward, $xp_reward, $user['uid']]);
|
||||||
|
|
||||||
|
echo json_encode(['status' => 'solved', 'earned_xp' => $xp_reward]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['error' => 'Endpunkt nicht gefunden']);
|
||||||
245
assets/app_auth.js
Normal file
245
assets/app_auth.js
Normal file
|
|
@ -0,0 +1,245 @@
|
||||||
|
/**
|
||||||
|
* 🔐 AUTHENTIFIZIERUNG & USER MANAGEMENT 🔐
|
||||||
|
* ==========================================
|
||||||
|
* Verarbeitet Login, Registrierung, JWT-Tokens und Lehrer-Klassenzuweisung.
|
||||||
|
* Mit automatischem Offline-Fallback (LocalStorage), falls kein Server läuft.
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
window.AUTH = {
|
||||||
|
token: localStorage.getItem("auth_token") || null,
|
||||||
|
user: JSON.parse(localStorage.getItem("auth_user") || "null"),
|
||||||
|
|
||||||
|
async register(name, email, password, role = "student") {
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/auth/register", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ name, email, password, role })
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok) throw new Error(data.error || "Registrierung fehlgeschlagen");
|
||||||
|
|
||||||
|
this.setSession(data.token, data.user);
|
||||||
|
return { success: true, user: data.user };
|
||||||
|
} catch (err) {
|
||||||
|
// Fallback für statische Offline-Nutzung
|
||||||
|
if (err.message.includes("Failed to fetch")) {
|
||||||
|
const mockUser = { id: Date.now(), name, email, role, xp: 0, level: 1 };
|
||||||
|
this.setSession("offline_token_" + Date.now(), mockUser);
|
||||||
|
return { success: true, user: mockUser, offline: true };
|
||||||
|
}
|
||||||
|
return { success: false, error: err.message };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async login(email, password) {
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/auth/login", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ email, password })
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok) throw new Error(data.error || "Anmeldung fehlgeschlagen");
|
||||||
|
|
||||||
|
this.setSession(data.token, data.user);
|
||||||
|
return { success: true, user: data.user };
|
||||||
|
} catch (err) {
|
||||||
|
if (err.message.includes("Failed to fetch")) {
|
||||||
|
const mockUser = { id: 1, name: email.split("@")[0], email, role: "student", xp: 150, level: 2 };
|
||||||
|
this.setSession("offline_token", mockUser);
|
||||||
|
return { success: true, user: mockUser, offline: true };
|
||||||
|
}
|
||||||
|
return { success: false, error: err.message };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
logout() {
|
||||||
|
this.token = null;
|
||||||
|
this.user = null;
|
||||||
|
localStorage.removeItem("auth_token");
|
||||||
|
localStorage.removeItem("auth_user");
|
||||||
|
window.location.reload();
|
||||||
|
},
|
||||||
|
|
||||||
|
setSession(token, user) {
|
||||||
|
this.token = token;
|
||||||
|
this.user = user;
|
||||||
|
localStorage.setItem("auth_token", token);
|
||||||
|
localStorage.setItem("auth_user", JSON.stringify(user));
|
||||||
|
this.updateUI();
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchMe() {
|
||||||
|
if (!this.token) return null;
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/auth/me", {
|
||||||
|
headers: { Authorization: `Bearer ${this.token}` }
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
this.user = data.user;
|
||||||
|
localStorage.setItem("auth_user", JSON.stringify(data.user));
|
||||||
|
this.updateUI();
|
||||||
|
return data.user;
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
return this.user;
|
||||||
|
},
|
||||||
|
|
||||||
|
updateUI() {
|
||||||
|
const userBadge = document.getElementById("nav-auth-user");
|
||||||
|
const loginBtn = document.getElementById("nav-btn-login");
|
||||||
|
const registerBtn = document.getElementById("nav-btn-register");
|
||||||
|
const teacherLink = document.getElementById("nav-teacher-link");
|
||||||
|
|
||||||
|
if (this.user) {
|
||||||
|
if (userBadge) {
|
||||||
|
userBadge.style.display = "inline-flex";
|
||||||
|
userBadge.innerHTML = `<span>👤 ${this.user.name}</span> <span style="background: rgba(255,255,255,0.2); padding: 2px 8px; border-radius: 99px; font-size: 0.75rem;">${this.user.xp || 0} XP</span>`;
|
||||||
|
}
|
||||||
|
if (loginBtn) loginBtn.style.display = "none";
|
||||||
|
if (registerBtn) {
|
||||||
|
registerBtn.innerText = "Abmelden";
|
||||||
|
registerBtn.onclick = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.logout();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (teacherLink && this.user.role === "teacher") {
|
||||||
|
teacherLink.style.display = "inline-flex";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (userBadge) userBadge.style.display = "none";
|
||||||
|
if (loginBtn) loginBtn.style.display = "inline-flex";
|
||||||
|
if (registerBtn) {
|
||||||
|
registerBtn.innerText = "Konto erstellen";
|
||||||
|
registerBtn.onclick = () => window.openAuthModal("register");
|
||||||
|
}
|
||||||
|
if (teacherLink) teacherLink.style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auth Modal UI
|
||||||
|
window.openAuthModal = function (mode = "login") {
|
||||||
|
let modal = document.getElementById("auth-modal-overlay");
|
||||||
|
if (!modal) {
|
||||||
|
modal = document.createElement("div");
|
||||||
|
modal.id = "auth-modal-overlay";
|
||||||
|
modal.className = "certificate-overlay";
|
||||||
|
modal.innerHTML = `
|
||||||
|
<div class="certificate-container" style="max-width: 420px; padding: 28px;">
|
||||||
|
<h3 id="auth-modal-title" style="margin-top: 0; font-size: 1.35rem; color: var(--text-main);">🔑 Anmelden</h3>
|
||||||
|
<p id="auth-modal-sub" style="font-size: 0.88rem; color: var(--text-muted); margin-bottom: 18px;">Melde dich an, um deinen Code und deine XP in der Cloud zu speichern.</p>
|
||||||
|
|
||||||
|
<form id="auth-form" style="display: flex; flex-direction: column; gap: 12px;">
|
||||||
|
<div id="auth-field-name" style="display: none;">
|
||||||
|
<label style="font-size: 0.82rem; font-weight: 700; color: var(--text-main);">Dein Name:</label>
|
||||||
|
<input type="text" id="auth-input-name" style="width: 100%; padding: 8px 12px; border: 1px solid var(--border-color); border-radius: var(--radius-sm); font-size: 0.92rem;" placeholder="z.B. Anna Schmidt">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label style="font-size: 0.82rem; font-weight: 700; color: var(--text-main);">E-Mail-Adresse:</label>
|
||||||
|
<input type="email" id="auth-input-email" required style="width: 100%; padding: 8px 12px; border: 1px solid var(--border-color); border-radius: var(--radius-sm); font-size: 0.92rem;" placeholder="name@schule.de">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label style="font-size: 0.82rem; font-weight: 700; color: var(--text-main);">Passwort:</label>
|
||||||
|
<input type="password" id="auth-input-password" required minlength="6" style="width: 100%; padding: 8px 12px; border: 1px solid var(--border-color); border-radius: var(--radius-sm); font-size: 0.92rem;" placeholder="Mindestens 6 Zeichen">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="auth-field-role" style="display: none;">
|
||||||
|
<label style="font-size: 0.82rem; font-weight: 700; color: var(--text-main);">Rolle:</label>
|
||||||
|
<select id="auth-select-role" style="width: 100%; padding: 8px 12px; border: 1px solid var(--border-color); border-radius: var(--radius-sm); font-size: 0.92rem;">
|
||||||
|
<option value="student">🎓 Schüler / Selbstlerner</option>
|
||||||
|
<option value="teacher">👨🏫 Lehrkraft / Dozent</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="auth-error-msg" style="color: var(--danger); font-size: 0.82rem; display: none;"></div>
|
||||||
|
|
||||||
|
<button type="submit" id="auth-submit-btn" class="btn" style="margin-top: 8px; justify-content: center;">Anmelden</button>
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="document.getElementById('auth-modal-overlay').remove();" style="justify-content: center;">Abbrechen</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div style="margin-top: 14px; text-align: center; font-size: 0.84rem;">
|
||||||
|
<a href="#" id="auth-toggle-mode" style="color: var(--primary); text-decoration: none; font-weight: 600;">Noch kein Konto? Jetzt registrieren →</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
}
|
||||||
|
|
||||||
|
const titleEl = document.getElementById("auth-modal-title");
|
||||||
|
const nameField = document.getElementById("auth-field-name");
|
||||||
|
const roleField = document.getElementById("auth-field-role");
|
||||||
|
const submitBtn = document.getElementById("auth-submit-btn");
|
||||||
|
const toggleLink = document.getElementById("auth-toggle-mode");
|
||||||
|
const errorMsg = document.getElementById("auth-error-msg");
|
||||||
|
const form = document.getElementById("auth-form");
|
||||||
|
|
||||||
|
let isRegister = mode === "register";
|
||||||
|
|
||||||
|
function updateModalMode() {
|
||||||
|
if (isRegister) {
|
||||||
|
titleEl.innerText = "✨ Neues Konto erstellen";
|
||||||
|
nameField.style.display = "block";
|
||||||
|
roleField.style.display = "block";
|
||||||
|
submitBtn.innerText = "Konto registrieren";
|
||||||
|
toggleLink.innerText = "Bereits registriert? Hier anmelden →";
|
||||||
|
} else {
|
||||||
|
titleEl.innerText = "🔑 Anmelden";
|
||||||
|
nameField.style.display = "none";
|
||||||
|
roleField.style.display = "none";
|
||||||
|
submitBtn.innerText = "Anmelden";
|
||||||
|
toggleLink.innerText = "Noch kein Konto? Jetzt registrieren →";
|
||||||
|
}
|
||||||
|
errorMsg.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
updateModalMode();
|
||||||
|
|
||||||
|
toggleLink.onclick = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
isRegister = !isRegister;
|
||||||
|
updateModalMode();
|
||||||
|
};
|
||||||
|
|
||||||
|
form.onsubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
errorMsg.style.display = "none";
|
||||||
|
const email = document.getElementById("auth-input-email").value.trim();
|
||||||
|
const password = document.getElementById("auth-input-password").value;
|
||||||
|
const name = document.getElementById("auth-input-name")?.value.trim() || email.split("@")[0];
|
||||||
|
const role = document.getElementById("auth-select-role")?.value || "student";
|
||||||
|
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
submitBtn.innerText = "Bitte warten...";
|
||||||
|
|
||||||
|
let result;
|
||||||
|
if (isRegister) {
|
||||||
|
result = await window.AUTH.register(name, email, password, role);
|
||||||
|
} else {
|
||||||
|
result = await window.AUTH.login(email, password);
|
||||||
|
}
|
||||||
|
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
submitBtn.innerText = isRegister ? "Konto registrieren" : "Anmelden";
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
document.getElementById("auth-modal-overlay")?.remove();
|
||||||
|
alert(`Willkommen zurück, ${result.user.name}! 👋`);
|
||||||
|
} else {
|
||||||
|
errorMsg.innerText = result.error || "Ein Fehler ist aufgetreten";
|
||||||
|
errorMsg.style.display = "block";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
window.AUTH.updateUI();
|
||||||
|
window.AUTH.fetchMe();
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
@ -11,7 +11,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-29T16:00:34.190254"
|
"zuletzt_aktualisiert": "2026-08-29T20:50:51.073181"
|
||||||
};
|
};
|
||||||
window.GAMIFICATION_BADGES_KATALOG = {
|
window.GAMIFICATION_BADGES_KATALOG = {
|
||||||
"rechenkonegin": {
|
"rechenkonegin": {
|
||||||
|
|
|
||||||
333
assets/monaco_ide.js
Normal file
333
assets/monaco_ide.js
Normal file
|
|
@ -0,0 +1,333 @@
|
||||||
|
/**
|
||||||
|
* ⚡ MONACO WEB-IDE & PYODIDE TEST-RUNNER ENGINE ⚡
|
||||||
|
* =================================================
|
||||||
|
* Interaktive Split-Screen Entwicklungsumgebung mit:
|
||||||
|
* - Monaco Editor (VS Code Engine mit Autocomplete & Syntax-Highlighting)
|
||||||
|
* - In-Browser Pyodide WebAssembly Python Runner (95% Client-Execution)
|
||||||
|
* - Automatischer Test-Auswertung mit XP-Vergabe
|
||||||
|
* - Cloud Auto-Save
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
let monacoEditor = null;
|
||||||
|
let pyodideInstance = null;
|
||||||
|
let pyodideLoading = false;
|
||||||
|
let currentChapterData = null;
|
||||||
|
let autoSaveTimeout = null;
|
||||||
|
|
||||||
|
// 1. Initialisiere Monaco Editor via CDN
|
||||||
|
function initMonacoEditor(initialCode = "") {
|
||||||
|
const editorContainer = document.getElementById("monaco-editor-container");
|
||||||
|
if (!editorContainer) return;
|
||||||
|
|
||||||
|
if (window.monaco) {
|
||||||
|
createMonacoInstance(initialCode);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Monaco CDN Loader
|
||||||
|
const loaderScript = document.createElement("script");
|
||||||
|
loaderScript.src = "https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs/loader.min.js";
|
||||||
|
loaderScript.onload = () => {
|
||||||
|
window.require.config({
|
||||||
|
paths: { vs: "https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs" }
|
||||||
|
});
|
||||||
|
window.require(["vs/editor/editor.main"], () => {
|
||||||
|
createMonacoInstance(initialCode);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
loaderScript.onerror = () => {
|
||||||
|
// Fallback: Einfache Textarea falls CDN offline
|
||||||
|
editorContainer.innerHTML = `<textarea id="fallback-code-editor" style="width: 100%; height: 100%; background: #0b0f19; color: #f8fafc; font-family: monospace; font-size: 14px; padding: 14px; border: none; outline: none;">${initialCode}</textarea>`;
|
||||||
|
};
|
||||||
|
document.head.appendChild(loaderScript);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMonacoInstance(initialCode) {
|
||||||
|
const editorContainer = document.getElementById("monaco-editor-container");
|
||||||
|
if (!editorContainer) return;
|
||||||
|
editorContainer.innerHTML = "";
|
||||||
|
|
||||||
|
monacoEditor = window.monaco.editor.create(editorContainer, {
|
||||||
|
value: initialCode,
|
||||||
|
language: "python",
|
||||||
|
theme: "vs-dark",
|
||||||
|
fontSize: 14,
|
||||||
|
fontFamily: "'JetBrains Mono', 'Fira Code', Consolas, monospace",
|
||||||
|
automaticLayout: true,
|
||||||
|
minimap: { enabled: false },
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
tabSize: 4,
|
||||||
|
insertSpaces: true,
|
||||||
|
lineNumbers: "on",
|
||||||
|
bracketPairColorization: { enabled: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-Save Trigger beim Tippen
|
||||||
|
monacoEditor.onDidChangeModelContent(() => {
|
||||||
|
triggerAutoSave();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Pyodide WebAssembly Initialisierung
|
||||||
|
async function getPyodide() {
|
||||||
|
if (pyodideInstance) return pyodideInstance;
|
||||||
|
if (pyodideLoading) {
|
||||||
|
while (pyodideLoading) {
|
||||||
|
await new Promise((r) => setTimeout(r, 100));
|
||||||
|
}
|
||||||
|
return pyodideInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
pyodideLoading = true;
|
||||||
|
logToTerminal("⏳ Lade Python WebAssembly Engine (Pyodide)... Bitte kurz warten.\n", "info");
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!window.loadPyodide) {
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const script = document.createElement("script");
|
||||||
|
script.src = "https://cdn.jsdelivr.net/pyodide/v0.25.0/full/pyodide.js";
|
||||||
|
script.onload = resolve;
|
||||||
|
script.onerror = reject;
|
||||||
|
document.head.appendChild(script);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pyodideInstance = await window.loadPyodide({
|
||||||
|
stdout: (text) => logToTerminal(text + "\n", "stdout"),
|
||||||
|
stderr: (text) => logToTerminal(text + "\n", "stderr")
|
||||||
|
});
|
||||||
|
|
||||||
|
logToTerminal("✅ Python WebAssembly erfolgreich bereitgestellt!\n\n", "success");
|
||||||
|
} catch (err) {
|
||||||
|
logToTerminal(`❌ Fehler beim Laden von Pyodide: ${err.message}\n`, "error");
|
||||||
|
} finally {
|
||||||
|
pyodideLoading = false;
|
||||||
|
}
|
||||||
|
return pyodideInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Code-Ausführung (Run)
|
||||||
|
async function runCode() {
|
||||||
|
const code = getEditorCode();
|
||||||
|
clearTerminal();
|
||||||
|
logToTerminal("▶ Starte Python-Skript...\n----------------------------------------\n", "info");
|
||||||
|
|
||||||
|
const py = await getPyodide();
|
||||||
|
if (!py) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await py.runPythonAsync(code);
|
||||||
|
logToTerminal("\n----------------------------------------\n✨ Skript erfolgreich ausgeführt!\n", "success");
|
||||||
|
} catch (err) {
|
||||||
|
logToTerminal(`\n❌ Ausführungsfehler:\n${err.message}\n`, "error");
|
||||||
|
suggestErrorInterpreter(err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Unittest-Ausführung (Test Runner)
|
||||||
|
async function runTests() {
|
||||||
|
if (!currentChapterData || !currentChapterData.testCode) {
|
||||||
|
logToTerminal("❌ Keine Unittests für dieses Kapitel gefunden.\n", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userCode = getEditorCode();
|
||||||
|
clearTerminal();
|
||||||
|
logToTerminal("🧪 Führe automatisierte Unittests aus...\n----------------------------------------\n", "info");
|
||||||
|
|
||||||
|
const py = await getPyodide();
|
||||||
|
if (!py) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Schreibe aufgabe.py in das Pyodide-Dateisystem
|
||||||
|
py.FS.writeFile("aufgabe.py", userCode);
|
||||||
|
|
||||||
|
// 2. Führe test_aufgabe.py aus
|
||||||
|
const testRunnerScript = `
|
||||||
|
import unittest, io, sys
|
||||||
|
from unittest import TextTestRunner
|
||||||
|
|
||||||
|
for mod in list(sys.modules.keys()):
|
||||||
|
if mod in ('aufgabe', 'test_aufgabe'):
|
||||||
|
del sys.modules[mod]
|
||||||
|
|
||||||
|
import aufgabe
|
||||||
|
|
||||||
|
${currentChapterData.testCode}
|
||||||
|
|
||||||
|
suite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__])
|
||||||
|
stream = io.StringIO()
|
||||||
|
runner = TextTestRunner(stream=stream, verbosity=2)
|
||||||
|
result = runner.run(suite)
|
||||||
|
|
||||||
|
output_text = stream.getvalue()
|
||||||
|
success = result.wasSuccessful()
|
||||||
|
total_runs = result.testsRun
|
||||||
|
failures = len(result.failures)
|
||||||
|
errors = len(result.errors)
|
||||||
|
(success, total_runs, failures, errors, output_text)
|
||||||
|
`;
|
||||||
|
|
||||||
|
const [isSuccess, totalRuns, fails, errs, outputText] = await py.runPythonAsync(testRunnerScript);
|
||||||
|
|
||||||
|
logToTerminal(outputText + "\n", "stdout");
|
||||||
|
|
||||||
|
if (isSuccess) {
|
||||||
|
logToTerminal(`\n🎉 HERVORRAGEND! Alle ${totalRuns} Tests erfolgreich bestanden! (+100 XP)\n`, "success");
|
||||||
|
triggerSuccessCelebration();
|
||||||
|
recordChapterSolved();
|
||||||
|
} else {
|
||||||
|
logToTerminal(`\n⚠️ ${fails + errs} von ${totalRuns} Tests fehlgeschlagen. Überarbeite deinen Code!\n`, "warning");
|
||||||
|
suggestErrorInterpreter(outputText);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logToTerminal(`\n❌ Fehler beim Ausführen der Testsuite:\n${err.message}\n`, "error");
|
||||||
|
suggestErrorInterpreter(err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Hilfsfunktionen
|
||||||
|
function getEditorCode() {
|
||||||
|
if (monacoEditor) {
|
||||||
|
return monacoEditor.getValue();
|
||||||
|
}
|
||||||
|
const fallback = document.getElementById("fallback-code-editor");
|
||||||
|
return fallback ? fallback.value : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function setEditorCode(code) {
|
||||||
|
if (monacoEditor) {
|
||||||
|
monacoEditor.setValue(code);
|
||||||
|
} else {
|
||||||
|
const fallback = document.getElementById("fallback-code-editor");
|
||||||
|
if (fallback) fallback.value = code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function logToTerminal(text, type = "stdout") {
|
||||||
|
const term = document.getElementById("workspace-terminal-output");
|
||||||
|
if (!term) return;
|
||||||
|
|
||||||
|
const span = document.createElement("span");
|
||||||
|
span.className = `term-line term-${type}`;
|
||||||
|
span.innerText = text;
|
||||||
|
term.appendChild(span);
|
||||||
|
term.scrollTop = term.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearTerminal() {
|
||||||
|
const term = document.getElementById("workspace-terminal-output");
|
||||||
|
if (term) term.innerHTML = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerAutoSave() {
|
||||||
|
clearTimeout(autoSaveTimeout);
|
||||||
|
autoSaveTimeout = setTimeout(() => {
|
||||||
|
saveProgressDraft();
|
||||||
|
}, 1500); // 1.5s nach letztem Tastenanschlag speichern
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveProgressDraft() {
|
||||||
|
if (!currentChapterData || !currentChapterData.chapterId) return;
|
||||||
|
const code = getEditorCode();
|
||||||
|
|
||||||
|
// 1. Lokal in LocalStorage
|
||||||
|
localStorage.setItem(`code_draft_${currentChapterData.chapterId}`, code);
|
||||||
|
|
||||||
|
// 2. In Backend-Cloud falls angemeldet
|
||||||
|
const token = localStorage.getItem("auth_token");
|
||||||
|
if (token) {
|
||||||
|
try {
|
||||||
|
await fetch("/api/progress/save", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
chapter_id: currentChapterData.chapterId,
|
||||||
|
code_draft: code
|
||||||
|
})
|
||||||
|
});
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveBadge = document.getElementById("editor-save-indicator");
|
||||||
|
if (saveBadge) {
|
||||||
|
saveBadge.innerText = "💾 Gespeichert";
|
||||||
|
saveBadge.style.opacity = "1";
|
||||||
|
setTimeout(() => {
|
||||||
|
saveBadge.style.opacity = "0.5";
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function recordChapterSolved() {
|
||||||
|
if (!currentChapterData || !currentChapterData.chapterId) return;
|
||||||
|
|
||||||
|
// 1. Lokale Gamification
|
||||||
|
if (window.addXP) {
|
||||||
|
window.addXP(100, currentChapterData.chapterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Backend Cloud
|
||||||
|
const token = localStorage.getItem("auth_token");
|
||||||
|
if (token) {
|
||||||
|
try {
|
||||||
|
await fetch("/api/progress/solve", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
chapter_id: currentChapterData.chapterId,
|
||||||
|
xp_reward: 100
|
||||||
|
})
|
||||||
|
});
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerSuccessCelebration() {
|
||||||
|
const banner = document.getElementById("success-modal-banner");
|
||||||
|
if (banner) {
|
||||||
|
banner.style.display = "flex";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function suggestErrorInterpreter(traceback) {
|
||||||
|
const errorTab = document.getElementById("tab-btn-interpreter");
|
||||||
|
const errorInput = document.getElementById("workspace-error-input");
|
||||||
|
if (errorInput) {
|
||||||
|
errorInput.value = traceback;
|
||||||
|
}
|
||||||
|
if (errorTab) {
|
||||||
|
errorTab.style.borderColor = "var(--danger)";
|
||||||
|
errorTab.innerHTML = "🩺 Fehler-Dolmetscher ⚠️";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Init Workspace Controller
|
||||||
|
window.initWorkspace = function (chapterData) {
|
||||||
|
currentChapterData = chapterData;
|
||||||
|
|
||||||
|
// Lade gespeicherten Code-Draft oder Startercode
|
||||||
|
const savedDraft = localStorage.getItem(`code_draft_${chapterData.chapterId}`);
|
||||||
|
const initialCode = savedDraft || chapterData.starterCode || "";
|
||||||
|
|
||||||
|
initMonacoEditor(initialCode);
|
||||||
|
|
||||||
|
// Event Listener für Buttons
|
||||||
|
document.getElementById("btn-run-code")?.addEventListener("click", runCode);
|
||||||
|
document.getElementById("btn-run-tests")?.addEventListener("click", runTests);
|
||||||
|
document.getElementById("btn-save-code")?.addEventListener("click", saveProgressDraft);
|
||||||
|
document.getElementById("btn-clear-terminal")?.addEventListener("click", clearTerminal);
|
||||||
|
document.getElementById("btn-reset-code")?.addEventListener("click", () => {
|
||||||
|
if (confirm("Möchtest du deinen Code wirklich auf den Original-Startercode zurücksetzen?")) {
|
||||||
|
setEditorCode(chapterData.starterCode || "");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
})();
|
||||||
538
index.html
538
index.html
|
|
@ -9,7 +9,7 @@
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<div class="app-layout">
|
<div class="app-layout">
|
||||||
<aside class="app-sidebar">
|
<aside class="app-sidebar">
|
||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
<a href="./index.html" class="sidebar-brand-link">
|
<a href="./index.html" class="sidebar-brand-link">
|
||||||
<div class="sidebar-logo">🐍</div>
|
<div class="sidebar-logo">🐍</div>
|
||||||
|
|
@ -176,121 +176,129 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<div class="app-main">
|
<div class="app-main">
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
<div class="header-container">
|
<div class="header-container">
|
||||||
<div class="header-badge">🐍 Python Nachhilfe- & Selbstlern-Portal</div>
|
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px; margin-bottom: 12px;">
|
||||||
<h1>Vom Einsteiger zum Software-Entwickler</h1>
|
<div class="header-badge">🐍 Python Online-Lernplattform & Web-IDE</div>
|
||||||
<p>3 didaktisch strukturierte Lehrpfade: Prozedurale Grundlagen • Objektorientierung • Professionelle Werkzeuge (Git, TDD, Tkinter, Persistenz)</p>
|
<div style="display: flex; gap: 8px; align-items: center;">
|
||||||
|
<span id="nav-auth-user" style="display: none; font-size: 0.85rem; font-weight: 700; color: white;"></span>
|
||||||
<nav class="nav-links" style="margin-top: 18px;">
|
<button type="button" class="btn" id="nav-btn-login" style="padding: 5px 12px; font-size: 0.8rem; background: rgba(255,255,255,0.15);" onclick="window.openAuthModal('login')">🔑 Anmelden</button>
|
||||||
<a href="#tutorial">📖 Anleitung & Workflow</a>
|
<button type="button" class="btn" id="nav-btn-register" style="padding: 5px 12px; font-size: 0.8rem; background: var(--primary);" onclick="window.openAuthModal('register')">Konto erstellen</button>
|
||||||
<a href="#lehrpfad-1">🌱 1. Grundlagen</a>
|
<a href="teacher.html" class="btn" id="nav-teacher-link" style="padding: 5px 12px; font-size: 0.8rem; background: #059669; display: none;">👨🏫 Lehrerbereich</a>
|
||||||
<a href="#lehrpfad-2">🏗️ 2. OOP Einstieg</a>
|
</div>
|
||||||
<a href="#lehrpfad-3">🚀 3. Fortgeschritten</a>
|
|
||||||
<a href="#python-live-playground">⚡ Live-Playground</a>
|
|
||||||
<a href="#error-interpreter-widget">🩺 Fehler-Dolmetscher</a>
|
|
||||||
<a href="#zertifikate">🏆 Zertifikate</a>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main class="container">
|
|
||||||
|
|
||||||
<!-- Willkommen Card -->
|
|
||||||
<section class="card">
|
|
||||||
<h2>👋 Willkommen zu deiner Python-Lernreise!</h2>
|
|
||||||
<p>
|
|
||||||
Dieses interaktive Portal begleitet dich Schritt für Schritt von den allerersten Zeilen Code bis hin zu vollständigen, objektorientierten Desktop-Anwendungen. Jede Einheit bietet didaktische Alltagsanalogien, Subgoal-Schritte und <strong>automatische Unittests</strong> für sofortiges Feedback.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 14px; margin: 20px 0;">
|
|
||||||
<div style="background: #f8fafc; border-left: 4px solid #059669; padding: 14px; border-radius: var(--radius-sm);">
|
|
||||||
<span class="track-badge track-1-badge">Lehrpfad 1</span>
|
|
||||||
<h4 style="margin: 4px 0; color: #065f46;">🌱 Grundlagen der Programmierung</h4>
|
|
||||||
<p style="font-size: 0.85rem; color: var(--text-muted); margin: 0;">Rechnen, Variablen, Bedingungen, Schleifen, Funktionen, Listen, Dictionaries & Strings (G01–G10).</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div style="background: #f8fafc; border-left: 4px solid #4f46e5; padding: 14px; border-radius: var(--radius-sm);">
|
<h1>Vom Einsteiger zum Software-Entwickler</h1>
|
||||||
<span class="track-badge track-2-badge">Lehrpfad 2</span>
|
<p>3 didaktisch strukturierte Lehrpfade mit integrierter **Monaco Web-IDE**, In-Browser Python-Runner (Pyodide), Aufgaben-Checklisten & persistentem Entwickler-Pass.</p>
|
||||||
<h4 style="margin: 4px 0; color: #312e81;">🏗️ Einstieg in OOP & Tools</h4>
|
|
||||||
<p style="font-size: 0.85rem; color: var(--text-muted); margin: 0;">Klassen, __init__, Methoden, UML-Diagramme, VS Code, Git-Versionskontrolle & Tamagotchi (00–06).</p>
|
|
||||||
</div>
|
|
||||||
<div style="background: #f8fafc; border-left: 4px solid #7c3aed; padding: 14px; border-radius: var(--radius-sm);">
|
|
||||||
<span class="track-badge track-3-badge">Lehrpfad 3</span>
|
|
||||||
<h4 style="margin: 4px 0; color: #4c1d95;">🚀 Fortgeschrittenes OOP & GUIs</h4>
|
|
||||||
<p style="font-size: 0.85rem; color: var(--text-muted); margin: 0;">Speicher, Dunder-Methoden, Unit Tests (TDD), Vererbung, Exceptions, JSON, Tkinter & Masterprojekt (07–16).</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="box box-tipp">
|
|
||||||
<div class="box-title">📚 Lehrplan-Abstimmung:</div>
|
|
||||||
Alle Einheiten sind exakt auf das offizielle Unterrichtsskriptum abgestimmt. Detaillierter Leitfaden in <a href="LEHRPLAN.md" style="color: var(--primary); font-weight: bold;">LEHRPLAN.md</a>.
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 📖 Schritt-für-Schritt Tutorial -->
|
|
||||||
<section class="card" id="tutorial" style="border-left: 4px solid var(--primary);">
|
|
||||||
<h2>📖 Schritt-für-Schritt-Tutorial: So lernst du mit dem Portal</h2>
|
|
||||||
<p>
|
|
||||||
Dieses Portal basiert auf dem <strong>PRIMM-Modell</strong> der Informatikdidaktik (Predict • Read • Investigate • Modify • Make):
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div style="display: flex; flex-direction: column; gap: 16px; margin-top: 18px;">
|
|
||||||
|
|
||||||
<div style="display: flex; gap: 14px; align-items: flex-start;">
|
<nav class="nav-links" style="margin-top: 18px;">
|
||||||
<div style="background: var(--primary); color: white; width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 800; flex-shrink: 0;">1</div>
|
<a href="#tutorial">📖 Anleitung & Workflow</a>
|
||||||
<div>
|
<a href="#lehrpfad-1">🌱 1. Grundlagen</a>
|
||||||
<h4 style="margin: 0 0 4px 0; color: var(--text-main);">📥 Lokal nutzen oder online lernen</h4>
|
<a href="#lehrpfad-2">🏗️ 2. OOP Einstieg</a>
|
||||||
<p style="font-size: 0.9rem; color: var(--text-muted); margin: 0;">Du kannst das Repository lokal klonen (<code>git clone https://github.com/alexander-mueller/python-oop-lernportal.git</code>) oder die Seiten direkt offline per Doppelklick auf <code>index.html</code> öffnen.</p>
|
<a href="#lehrpfad-3">🚀 3. Fortgeschritten</a>
|
||||||
</div>
|
<a href="#python-live-playground">⚡ Live-Playground</a>
|
||||||
</div>
|
<a href="#error-interpreter-widget">🩺 Fehler-Dolmetscher</a>
|
||||||
|
<a href="#zertifikate">🏆 Zertifikate</a>
|
||||||
<div style="display: flex; gap: 14px; align-items: flex-start;">
|
</nav>
|
||||||
<div style="background: var(--primary); color: white; width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 800; flex-shrink: 0;">2</div>
|
|
||||||
<div>
|
|
||||||
<h4 style="margin: 0 0 4px 0; color: var(--text-main);">🧩 Kapitel lesen & Parsons-Puzzle lösen (+25 XP)</h4>
|
|
||||||
<p style="font-size: 0.9rem; color: var(--text-muted); margin: 0;">Lies die Alltagsanalogie im Kapitel-Guide und bringe die durcheinandergeratenen Code-Zeilen im interaktiven Puzzle per Klick in die richtige Reihenfolge.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display: flex; gap: 14px; align-items: flex-start;">
|
|
||||||
<div style="background: var(--primary); color: white; width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 800; flex-shrink: 0;">3</div>
|
|
||||||
<div>
|
|
||||||
<h4 style="margin: 0 0 4px 0; color: var(--text-main);">💻 Startercode lösen & Tests ausführen (+100 XP)</h4>
|
|
||||||
<p style="font-size: 0.9rem; color: var(--text-muted); margin: 0;">Öffne <code>aufgabe.py</code> im Editor, bearbeite die nummerierten Teilziele und teste deine Lösung im Terminal mit <code>python3 test_aufgabe.py</code>.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display: flex; gap: 14px; align-items: flex-start;">
|
|
||||||
<div style="background: var(--primary); color: white; width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 800; flex-shrink: 0;">4</div>
|
|
||||||
<div>
|
|
||||||
<h4 style="margin: 0 0 4px 0; color: var(--text-main);">🩺 Hilfe bei Fehlern</h4>
|
|
||||||
<p style="font-size: 0.9rem; color: var(--text-muted); margin: 0;">Kopiere rote Traceback-Fehlermeldungen aus dem Terminal in den untenstehenden <strong>Fehler-Dolmetscher</strong>, um verständliche Lösungsschritte auf Deutsch zu erhalten.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</header>
|
||||||
|
|
||||||
<!-- 🔍 Live-Suche -->
|
<main class="container">
|
||||||
<div class="search-bar-container">
|
|
||||||
<span style="font-size: 1.1rem;">🔍</span>
|
|
||||||
<input type="text" id="portal-search-input" class="search-input" placeholder="Kapitel oder Thema durchsuchen (z.B. Schleifen, Klassen, Git, Tkinter, Dunder, Exceptions)...">
|
|
||||||
<span id="search-results-count" style="font-size: 0.8rem; font-weight: 700; color: var(--primary); display: none;"></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- LEHRPFAD 1 -->
|
<!-- Willkommen Card -->
|
||||||
<section>
|
<section class="card">
|
||||||
<div class="section-header">
|
<h2>👋 Willkommen zu deiner Python-Lernreise!</h2>
|
||||||
<div>
|
<p>
|
||||||
<h2 id="lehrpfad-1">🌱 Lehrpfad 1: Grundlagen der Programmierung</h2>
|
Dieses Portal begleitet dich Schritt für Schritt von den ersten Schritten in Python bis hin zu vollständigen Desktop-Apps. Du kannst jedes Kapitel direkt in der <strong>integrierten Monaco Web-IDE</strong> im Browser programmieren und mit 1 Klick gegen automatische Tests prüfen!
|
||||||
<div class="section-desc">Datentypen, Kontrollstrukturen, Funktionen und Datenstrukturen (G01–G10).</div>
|
</p>
|
||||||
|
|
||||||
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 14px; margin: 20px 0;">
|
||||||
|
<div style="background: #f8fafc; border-left: 4px solid #059669; padding: 14px; border-radius: var(--radius-sm);">
|
||||||
|
<span class="track-badge track-1-badge">Lehrpfad 1</span>
|
||||||
|
<h4 style="margin: 4px 0; color: #065f46;">🌱 Grundlagen der Programmierung</h4>
|
||||||
|
<p style="font-size: 0.85rem; color: var(--text-muted); margin: 0;">Rechnen, Variablen, Bedingungen, Schleifen, Funktionen, Listen, Dictionaries & Strings (G01–G10).</p>
|
||||||
|
</div>
|
||||||
|
<div style="background: #f8fafc; border-left: 4px solid #4f46e5; padding: 14px; border-radius: var(--radius-sm);">
|
||||||
|
<span class="track-badge track-2-badge">Lehrpfad 2</span>
|
||||||
|
<h4 style="margin: 4px 0; color: #312e81;">🏗️ Einstieg in OOP & Tools</h4>
|
||||||
|
<p style="font-size: 0.85rem; color: var(--text-muted); margin: 0;">Klassen, __init__, Methoden, UML-Diagramme, VS Code, Git-Versionskontrolle & Tamagotchi (00–06).</p>
|
||||||
|
</div>
|
||||||
|
<div style="background: #f8fafc; border-left: 4px solid #7c3aed; padding: 14px; border-radius: var(--radius-sm);">
|
||||||
|
<span class="track-badge track-3-badge">Lehrpfad 3</span>
|
||||||
|
<h4 style="margin: 4px 0; color: #4c1d95;">🚀 Fortgeschrittenes OOP & GUIs</h4>
|
||||||
|
<p style="font-size: 0.85rem; color: var(--text-muted); margin: 0;">Speicher, Dunder-Methoden, Unit Tests (TDD), Vererbung, Exceptions, JSON, Tkinter & Masterprojekt (07–16).</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="track-badge track-1-badge">10 Kapitel</span>
|
|
||||||
|
<div class="box box-tipp">
|
||||||
|
<div class="box-title">📚 Lehrplan-Abstimmung:</div>
|
||||||
|
Alle Einheiten sind exakt auf das offizielle Unterrichtsskriptum abgestimmt. Detaillierter Leitfaden in <a href="LEHRPLAN.md" style="color: var(--primary); font-weight: bold;">LEHRPLAN.md</a>.
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 📖 Schritt-für-Schritt Tutorial -->
|
||||||
|
<section class="card" id="tutorial" style="border-left: 4px solid var(--primary);">
|
||||||
|
<h2>📖 Schritt-für-Schritt-Tutorial: So lernst du mit der Online-Plattform</h2>
|
||||||
|
<p>
|
||||||
|
Dieses Portal basiert auf dem <strong>PRIMM-Modell</strong> der Informatikdidaktik:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style="display: flex; flex-direction: column; gap: 16px; margin-top: 18px;">
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 14px; align-items: flex-start;">
|
||||||
|
<div style="background: var(--primary); color: white; width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 800; flex-shrink: 0;">1</div>
|
||||||
|
<div>
|
||||||
|
<h4 style="margin: 0 0 4px 0; color: var(--text-main);">💻 In Web-IDE öffnen</h4>
|
||||||
|
<p style="font-size: 0.9rem; color: var(--text-muted); margin: 0;">Klicke bei jedem Kapitel auf <strong>'In Web-IDE öffnen'</strong>, um den Monaco Code-Editor mit Startercode und Teilzielen direkt im Browser zu starten.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 14px; align-items: flex-start;">
|
||||||
|
<div style="background: var(--primary); color: white; width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 800; flex-shrink: 0;">2</div>
|
||||||
|
<div>
|
||||||
|
<h4 style="margin: 0 0 4px 0; color: var(--text-main);">🧩 Parsons-Puzzle lösen (+25 XP)</h4>
|
||||||
|
<p style="font-size: 0.9rem; color: var(--text-muted); margin: 0;">Bringe im Tab 'Puzzle' die durcheinandergeratenen Code-Zeilen in die richtige Reihenfolge, um ein intuitives Verständnis des Codes aufzubauen.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 14px; align-items: flex-start;">
|
||||||
|
<div style="background: var(--primary); color: white; width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 800; flex-shrink: 0;">3</div>
|
||||||
|
<div>
|
||||||
|
<h4 style="margin: 0 0 4px 0; color: var(--text-main);">🧪 Tests im Browser ausführen (+100 XP)</h4>
|
||||||
|
<p style="font-size: 0.9rem; color: var(--text-muted); margin: 0;">Klicke oben auf <strong>'🧪 Tests starten'</strong>. Die Python WebAssembly-Engine prüft deinen Code sofort und vergibt bei Erfolg XP und Trophäen.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 14px; align-items: flex-start;">
|
||||||
|
<div style="background: var(--primary); color: white; width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 800; flex-shrink: 0;">4</div>
|
||||||
|
<div>
|
||||||
|
<h4 style="margin: 0 0 4px 0; color: var(--text-main);">🩺 Hilfe bei Fehlern & Auto-Save</h4>
|
||||||
|
<p style="font-size: 0.9rem; color: var(--text-muted); margin: 0;">Dein Code wird automatisch in deinem Konto gespeichert. Tritt ein Fehler auf, erhältst du im Fehler-Dolmetscher sofort eine verständliche Erklärung auf Deutsch.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 🔍 Live-Suche -->
|
||||||
|
<div class="search-bar-container">
|
||||||
|
<span style="font-size: 1.1rem;">🔍</span>
|
||||||
|
<input type="text" id="portal-search-input" class="search-input" placeholder="Kapitel oder Thema durchsuchen (z.B. Schleifen, Klassen, Git, Tkinter, Dunder, Exceptions)...">
|
||||||
|
<span id="search-results-count" style="font-size: 0.8rem; font-weight: 700; color: var(--primary); display: none;"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="roadmap-grid">
|
|
||||||
|
<!-- LEHRPFAD 1 -->
|
||||||
|
<section>
|
||||||
|
<div class="section-header">
|
||||||
|
<div>
|
||||||
|
<h2 id="lehrpfad-1">🌱 Lehrpfad 1: Grundlagen der Programmierung</h2>
|
||||||
|
<div class="section-desc">Datentypen, Kontrollstrukturen, Funktionen und Datenstrukturen (G01–G10).</div>
|
||||||
|
</div>
|
||||||
|
<span class="track-badge track-1-badge">10 Kapitel</span>
|
||||||
|
</div>
|
||||||
|
<div class="roadmap-grid">
|
||||||
|
|
||||||
<div class="roadmap-card" style="border-left: 4px solid #059669;">
|
<div class="roadmap-card" style="border-left: 4px solid #059669;">
|
||||||
<div>
|
<div>
|
||||||
<span class="roadmap-number" style="background: #05966918; color: #059669;">G01</span>
|
<span class="roadmap-number" style="background: #05966918; color: #059669;">G01</span>
|
||||||
|
|
@ -298,7 +306,8 @@
|
||||||
<div class="roadmap-desc">Zahlen (<code>int</code>, <code>float</code>), Grundrechenarten, Ganzzahldivision <code>//</code>, Modulo <code>%</code>, Potenzen <code>**</code> & print().</div>
|
<div class="roadmap-desc">Zahlen (<code>int</code>, <code>float</code>), Grundrechenarten, Ganzzahldivision <code>//</code>, Modulo <code>%</code>, Potenzen <code>**</code> & print().</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<a href="lehrpfad_1_grundlagen/01_erste_schritte_taschenrechner/index.html" class="card-primary-btn" style="background: #059669;">📖 Kapitel öffnen →</a>
|
<a href="workspace.html?track=lehrpfad_1_grundlagen&chapter=01_erste_schritte_taschenrechner" class="card-primary-btn" style="background: #059669;">💻 In Web-IDE öffnen →</a>
|
||||||
|
<a href="lehrpfad_1_grundlagen/01_erste_schritte_taschenrechner/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
|
||||||
|
|
||||||
<div class="card-sublinks">
|
<div class="card-sublinks">
|
||||||
<a href="lehrpfad_1_grundlagen/01_erste_schritte_taschenrechner/aufgabe.py" download>📄 aufgabe.py</a>
|
<a href="lehrpfad_1_grundlagen/01_erste_schritte_taschenrechner/aufgabe.py" download>📄 aufgabe.py</a>
|
||||||
|
|
@ -314,7 +323,8 @@
|
||||||
<div class="roadmap-desc">Typen (<code>str</code>, <code>int</code>, <code>float</code>, <code>bool</code>), Typumwandlung (Casting) & Vermeidung typischer Typfehler.</div>
|
<div class="roadmap-desc">Typen (<code>str</code>, <code>int</code>, <code>float</code>, <code>bool</code>), Typumwandlung (Casting) & Vermeidung typischer Typfehler.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<a href="lehrpfad_1_grundlagen/02_variablen_und_datentypen/index.html" class="card-primary-btn" style="background: #059669;">📖 Kapitel öffnen →</a>
|
<a href="workspace.html?track=lehrpfad_1_grundlagen&chapter=02_variablen_und_datentypen" class="card-primary-btn" style="background: #059669;">💻 In Web-IDE öffnen →</a>
|
||||||
|
<a href="lehrpfad_1_grundlagen/02_variablen_und_datentypen/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
|
||||||
|
|
||||||
<div class="card-sublinks">
|
<div class="card-sublinks">
|
||||||
<a href="lehrpfad_1_grundlagen/02_variablen_und_datentypen/aufgabe.py" download>📄 aufgabe.py</a>
|
<a href="lehrpfad_1_grundlagen/02_variablen_und_datentypen/aufgabe.py" download>📄 aufgabe.py</a>
|
||||||
|
|
@ -330,7 +340,8 @@
|
||||||
<div class="roadmap-desc">Tastatureingaben mit <code>input()</code>, Textformatierung mit modernen f-Strings und Parametern.</div>
|
<div class="roadmap-desc">Tastatureingaben mit <code>input()</code>, Textformatierung mit modernen f-Strings und Parametern.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<a href="lehrpfad_1_grundlagen/03_ein_und_ausgabe/index.html" class="card-primary-btn" style="background: #059669;">📖 Kapitel öffnen →</a>
|
<a href="workspace.html?track=lehrpfad_1_grundlagen&chapter=03_ein_und_ausgabe" class="card-primary-btn" style="background: #059669;">💻 In Web-IDE öffnen →</a>
|
||||||
|
<a href="lehrpfad_1_grundlagen/03_ein_und_ausgabe/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
|
||||||
|
|
||||||
<div class="card-sublinks">
|
<div class="card-sublinks">
|
||||||
<a href="lehrpfad_1_grundlagen/03_ein_und_ausgabe/aufgabe.py" download>📄 aufgabe.py</a>
|
<a href="lehrpfad_1_grundlagen/03_ein_und_ausgabe/aufgabe.py" download>📄 aufgabe.py</a>
|
||||||
|
|
@ -346,7 +357,8 @@
|
||||||
<div class="roadmap-desc">Entscheidungen treffen mit <code>if</code>, <code>elif</code>, <code>else</code>, Vergleichsoperatoren & logischen Verknüpfungen.</div>
|
<div class="roadmap-desc">Entscheidungen treffen mit <code>if</code>, <code>elif</code>, <code>else</code>, Vergleichsoperatoren & logischen Verknüpfungen.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<a href="lehrpfad_1_grundlagen/04_verzweigungen_und_bedingungen/index.html" class="card-primary-btn" style="background: #059669;">📖 Kapitel öffnen →</a>
|
<a href="workspace.html?track=lehrpfad_1_grundlagen&chapter=04_verzweigungen_und_bedingungen" class="card-primary-btn" style="background: #059669;">💻 In Web-IDE öffnen →</a>
|
||||||
|
<a href="lehrpfad_1_grundlagen/04_verzweigungen_und_bedingungen/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
|
||||||
|
|
||||||
<div class="card-sublinks">
|
<div class="card-sublinks">
|
||||||
<a href="lehrpfad_1_grundlagen/04_verzweigungen_und_bedingungen/aufgabe.py" download>📄 aufgabe.py</a>
|
<a href="lehrpfad_1_grundlagen/04_verzweigungen_und_bedingungen/aufgabe.py" download>📄 aufgabe.py</a>
|
||||||
|
|
@ -362,7 +374,8 @@
|
||||||
<div class="roadmap-desc">Zählschleifen <code>for ... in range()</code>, Bedingungsschleifen <code>while</code>, <code>break</code> & <code>continue</code>.</div>
|
<div class="roadmap-desc">Zählschleifen <code>for ... in range()</code>, Bedingungsschleifen <code>while</code>, <code>break</code> & <code>continue</code>.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<a href="lehrpfad_1_grundlagen/05_schleifen_und_wiederholungen/index.html" class="card-primary-btn" style="background: #059669;">📖 Kapitel öffnen →</a>
|
<a href="workspace.html?track=lehrpfad_1_grundlagen&chapter=05_schleifen_und_wiederholungen" class="card-primary-btn" style="background: #059669;">💻 In Web-IDE öffnen →</a>
|
||||||
|
<a href="lehrpfad_1_grundlagen/05_schleifen_und_wiederholungen/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
|
||||||
|
|
||||||
<div class="card-sublinks">
|
<div class="card-sublinks">
|
||||||
<a href="lehrpfad_1_grundlagen/05_schleifen_und_wiederholungen/aufgabe.py" download>📄 aufgabe.py</a>
|
<a href="lehrpfad_1_grundlagen/05_schleifen_und_wiederholungen/aufgabe.py" download>📄 aufgabe.py</a>
|
||||||
|
|
@ -378,7 +391,8 @@
|
||||||
<div class="roadmap-desc">Eigene Funktionen definieren mit <code>def</code>, <code>return</code>, Default-Parameter und Importe (<code>math</code>, <code>random</code>).</div>
|
<div class="roadmap-desc">Eigene Funktionen definieren mit <code>def</code>, <code>return</code>, Default-Parameter und Importe (<code>math</code>, <code>random</code>).</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<a href="lehrpfad_1_grundlagen/06_funktionen_und_module/index.html" class="card-primary-btn" style="background: #059669;">📖 Kapitel öffnen →</a>
|
<a href="workspace.html?track=lehrpfad_1_grundlagen&chapter=06_funktionen_und_module" class="card-primary-btn" style="background: #059669;">💻 In Web-IDE öffnen →</a>
|
||||||
|
<a href="lehrpfad_1_grundlagen/06_funktionen_und_module/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
|
||||||
|
|
||||||
<div class="card-sublinks">
|
<div class="card-sublinks">
|
||||||
<a href="lehrpfad_1_grundlagen/06_funktionen_und_module/aufgabe.py" download>📄 aufgabe.py</a>
|
<a href="lehrpfad_1_grundlagen/06_funktionen_und_module/aufgabe.py" download>📄 aufgabe.py</a>
|
||||||
|
|
@ -394,7 +408,8 @@
|
||||||
<div class="roadmap-desc">Listen erstellen, Indexzugriff <code>[0]</code>, Slicing <code>[1:4]</code>, Methoden (<code>append</code>, <code>pop</code>) & Schleifen.</div>
|
<div class="roadmap-desc">Listen erstellen, Indexzugriff <code>[0]</code>, Slicing <code>[1:4]</code>, Methoden (<code>append</code>, <code>pop</code>) & Schleifen.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<a href="lehrpfad_1_grundlagen/07_listen_und_sequenzen/index.html" class="card-primary-btn" style="background: #059669;">📖 Kapitel öffnen →</a>
|
<a href="workspace.html?track=lehrpfad_1_grundlagen&chapter=07_listen_und_sequenzen" class="card-primary-btn" style="background: #059669;">💻 In Web-IDE öffnen →</a>
|
||||||
|
<a href="lehrpfad_1_grundlagen/07_listen_und_sequenzen/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
|
||||||
|
|
||||||
<div class="card-sublinks">
|
<div class="card-sublinks">
|
||||||
<a href="lehrpfad_1_grundlagen/07_listen_und_sequenzen/aufgabe.py" download>📄 aufgabe.py</a>
|
<a href="lehrpfad_1_grundlagen/07_listen_und_sequenzen/aufgabe.py" download>📄 aufgabe.py</a>
|
||||||
|
|
@ -410,7 +425,8 @@
|
||||||
<div class="roadmap-desc">String-Methoden (<code>split</code>, <code>join</code>, <code>strip</code>, <code>replace</code>), Groß/Klein und Zeichenanalysen.</div>
|
<div class="roadmap-desc">String-Methoden (<code>split</code>, <code>join</code>, <code>strip</code>, <code>replace</code>), Groß/Klein und Zeichenanalysen.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<a href="lehrpfad_1_grundlagen/08_textverarbeitung_und_strings/index.html" class="card-primary-btn" style="background: #059669;">📖 Kapitel öffnen →</a>
|
<a href="workspace.html?track=lehrpfad_1_grundlagen&chapter=08_textverarbeitung_und_strings" class="card-primary-btn" style="background: #059669;">💻 In Web-IDE öffnen →</a>
|
||||||
|
<a href="lehrpfad_1_grundlagen/08_textverarbeitung_und_strings/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
|
||||||
|
|
||||||
<div class="card-sublinks">
|
<div class="card-sublinks">
|
||||||
<a href="lehrpfad_1_grundlagen/08_textverarbeitung_und_strings/aufgabe.py" download>📄 aufgabe.py</a>
|
<a href="lehrpfad_1_grundlagen/08_textverarbeitung_und_strings/aufgabe.py" download>📄 aufgabe.py</a>
|
||||||
|
|
@ -426,7 +442,8 @@
|
||||||
<div class="roadmap-desc">Schlüssel-Wert-Paare <code>dict</code>, sicherer Zugriff mit <code>.get()</code>, eindeutige Mengen <code>set</code> & Operationen.</div>
|
<div class="roadmap-desc">Schlüssel-Wert-Paare <code>dict</code>, sicherer Zugriff mit <code>.get()</code>, eindeutige Mengen <code>set</code> & Operationen.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<a href="lehrpfad_1_grundlagen/09_dictionaries_und_sets/index.html" class="card-primary-btn" style="background: #059669;">📖 Kapitel öffnen →</a>
|
<a href="workspace.html?track=lehrpfad_1_grundlagen&chapter=09_dictionaries_und_sets" class="card-primary-btn" style="background: #059669;">💻 In Web-IDE öffnen →</a>
|
||||||
|
<a href="lehrpfad_1_grundlagen/09_dictionaries_und_sets/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
|
||||||
|
|
||||||
<div class="card-sublinks">
|
<div class="card-sublinks">
|
||||||
<a href="lehrpfad_1_grundlagen/09_dictionaries_und_sets/aufgabe.py" download>📄 aufgabe.py</a>
|
<a href="lehrpfad_1_grundlagen/09_dictionaries_und_sets/aufgabe.py" download>📄 aufgabe.py</a>
|
||||||
|
|
@ -442,7 +459,8 @@
|
||||||
<div class="roadmap-desc">Kompakte List-Comprehensions, Datumsberechnungen mit <code>datetime</code> & grundlegende Algorithmen.</div>
|
<div class="roadmap-desc">Kompakte List-Comprehensions, Datumsberechnungen mit <code>datetime</code> & grundlegende Algorithmen.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<a href="lehrpfad_1_grundlagen/10_comprehensions_datum_algorithmen/index.html" class="card-primary-btn" style="background: #059669;">📖 Kapitel öffnen →</a>
|
<a href="workspace.html?track=lehrpfad_1_grundlagen&chapter=10_comprehensions_datum_algorithmen" class="card-primary-btn" style="background: #059669;">💻 In Web-IDE öffnen →</a>
|
||||||
|
<a href="lehrpfad_1_grundlagen/10_comprehensions_datum_algorithmen/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
|
||||||
|
|
||||||
<div class="card-sublinks">
|
<div class="card-sublinks">
|
||||||
<a href="lehrpfad_1_grundlagen/10_comprehensions_datum_algorithmen/aufgabe.py" download>📄 aufgabe.py</a>
|
<a href="lehrpfad_1_grundlagen/10_comprehensions_datum_algorithmen/aufgabe.py" download>📄 aufgabe.py</a>
|
||||||
|
|
@ -451,20 +469,20 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- LEHRPFAD 2 -->
|
|
||||||
<section>
|
|
||||||
<div class="section-header">
|
|
||||||
<div>
|
|
||||||
<h2 id="lehrpfad-2">🏗️ Lehrpfad 2: Einstieg in OOP & Entwickler-Tools</h2>
|
|
||||||
<div class="section-desc">Klassen, Konstruktoren, Methoden, Komposition, VS Code, Git & Tamagotchi (00–06).</div>
|
|
||||||
</div>
|
</div>
|
||||||
<span class="track-badge track-2-badge">9 Module</span>
|
</section>
|
||||||
</div>
|
|
||||||
<div class="roadmap-grid">
|
<!-- LEHRPFAD 2 -->
|
||||||
|
<section>
|
||||||
|
<div class="section-header">
|
||||||
|
<div>
|
||||||
|
<h2 id="lehrpfad-2">🏗️ Lehrpfad 2: Einstieg in OOP & Entwickler-Tools</h2>
|
||||||
|
<div class="section-desc">Klassen, Konstruktoren, Methoden, Komposition, VS Code, Git & Tamagotchi (00–06).</div>
|
||||||
|
</div>
|
||||||
|
<span class="track-badge track-2-badge">9 Module</span>
|
||||||
|
</div>
|
||||||
|
<div class="roadmap-grid">
|
||||||
|
|
||||||
<div class="roadmap-card" style="border-left: 4px solid #4f46e5;">
|
<div class="roadmap-card" style="border-left: 4px solid #4f46e5;">
|
||||||
<div>
|
<div>
|
||||||
<span class="roadmap-number" style="background: #4f46e518; color: #4f46e5;">OOP 00</span>
|
<span class="roadmap-number" style="background: #4f46e518; color: #4f46e5;">OOP 00</span>
|
||||||
|
|
@ -472,7 +490,8 @@
|
||||||
<div class="roadmap-desc">Typische Syntax-, Type- und Index-Fehler verstehen, Fehlermeldungen lesen und gezielt beheben.</div>
|
<div class="roadmap-desc">Typische Syntax-, Type- und Index-Fehler verstehen, Fehlermeldungen lesen und gezielt beheben.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<a href="lehrpfad_2_oop_einstieg/00_fehlersuche_und_grundlagen/index.html" class="card-primary-btn" style="background: #4f46e5;">📖 Kapitel öffnen →</a>
|
<a href="workspace.html?track=lehrpfad_2_oop_einstieg&chapter=00_fehlersuche_und_grundlagen" class="card-primary-btn" style="background: #4f46e5;">💻 In Web-IDE öffnen →</a>
|
||||||
|
<a href="lehrpfad_2_oop_einstieg/00_fehlersuche_und_grundlagen/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
|
||||||
|
|
||||||
<div class="card-sublinks">
|
<div class="card-sublinks">
|
||||||
<a href="lehrpfad_2_oop_einstieg/00_fehlersuche_und_grundlagen/aufgabe.py" download>📄 aufgabe.py</a>
|
<a href="lehrpfad_2_oop_einstieg/00_fehlersuche_und_grundlagen/aufgabe.py" download>📄 aufgabe.py</a>
|
||||||
|
|
@ -488,7 +507,8 @@
|
||||||
<div class="roadmap-desc">Objektorientierung verstehen: Vom Bauplan zum fertigen Objekt (Instanz) im Speicher.</div>
|
<div class="roadmap-desc">Objektorientierung verstehen: Vom Bauplan zum fertigen Objekt (Instanz) im Speicher.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<a href="lehrpfad_2_oop_einstieg/01_einstieg_klassen/index.html" class="card-primary-btn" style="background: #4f46e5;">📖 Kapitel öffnen →</a>
|
<a href="workspace.html?track=lehrpfad_2_oop_einstieg&chapter=01_einstieg_klassen" class="card-primary-btn" style="background: #4f46e5;">💻 In Web-IDE öffnen →</a>
|
||||||
|
<a href="lehrpfad_2_oop_einstieg/01_einstieg_klassen/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
|
||||||
|
|
||||||
<div class="card-sublinks">
|
<div class="card-sublinks">
|
||||||
<a href="lehrpfad_2_oop_einstieg/01_einstieg_klassen/aufgabe.py" download>📄 aufgabe.py</a>
|
<a href="lehrpfad_2_oop_einstieg/01_einstieg_klassen/aufgabe.py" download>📄 aufgabe.py</a>
|
||||||
|
|
@ -504,7 +524,8 @@
|
||||||
<div class="roadmap-desc">Objekte initialisieren, Instanzvariablen mit <code>self</code> anlegen und Parameter übergeben.</div>
|
<div class="roadmap-desc">Objekte initialisieren, Instanzvariablen mit <code>self</code> anlegen und Parameter übergeben.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<a href="lehrpfad_2_oop_einstieg/02_init_und_self/index.html" class="card-primary-btn" style="background: #4f46e5;">📖 Kapitel öffnen →</a>
|
<a href="workspace.html?track=lehrpfad_2_oop_einstieg&chapter=02_init_und_self" class="card-primary-btn" style="background: #4f46e5;">💻 In Web-IDE öffnen →</a>
|
||||||
|
<a href="lehrpfad_2_oop_einstieg/02_init_und_self/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
|
||||||
|
|
||||||
<div class="card-sublinks">
|
<div class="card-sublinks">
|
||||||
<a href="lehrpfad_2_oop_einstieg/02_init_und_self/aufgabe.py" download>📄 aufgabe.py</a>
|
<a href="lehrpfad_2_oop_einstieg/02_init_und_self/aufgabe.py" download>📄 aufgabe.py</a>
|
||||||
|
|
@ -520,7 +541,8 @@
|
||||||
<div class="roadmap-desc">Objekten Fähigkeiten begeben: Methoden definieren, Zustand verändern und Werte berechnen.</div>
|
<div class="roadmap-desc">Objekten Fähigkeiten begeben: Methoden definieren, Zustand verändern und Werte berechnen.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<a href="lehrpfad_2_oop_einstieg/03_methoden_und_verhalten/index.html" class="card-primary-btn" style="background: #4f46e5;">📖 Kapitel öffnen →</a>
|
<a href="workspace.html?track=lehrpfad_2_oop_einstieg&chapter=03_methoden_und_verhalten" class="card-primary-btn" style="background: #4f46e5;">💻 In Web-IDE öffnen →</a>
|
||||||
|
<a href="lehrpfad_2_oop_einstieg/03_methoden_und_verhalten/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
|
||||||
|
|
||||||
<div class="card-sublinks">
|
<div class="card-sublinks">
|
||||||
<a href="lehrpfad_2_oop_einstieg/03_methoden_und_verhalten/aufgabe.py" download>📄 aufgabe.py</a>
|
<a href="lehrpfad_2_oop_einstieg/03_methoden_und_verhalten/aufgabe.py" download>📄 aufgabe.py</a>
|
||||||
|
|
@ -536,7 +558,8 @@
|
||||||
<div class="roadmap-desc">Magische Dunder-Methode <code>__str__</code> für saubere Textausgaben beim Drucken mit <code>print()</code>.</div>
|
<div class="roadmap-desc">Magische Dunder-Methode <code>__str__</code> für saubere Textausgaben beim Drucken mit <code>print()</code>.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<a href="lehrpfad_2_oop_einstieg/04_str_und_darstellung/index.html" class="card-primary-btn" style="background: #4f46e5;">📖 Kapitel öffnen →</a>
|
<a href="workspace.html?track=lehrpfad_2_oop_einstieg&chapter=04_str_und_darstellung" class="card-primary-btn" style="background: #4f46e5;">💻 In Web-IDE öffnen →</a>
|
||||||
|
<a href="lehrpfad_2_oop_einstieg/04_str_und_darstellung/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
|
||||||
|
|
||||||
<div class="card-sublinks">
|
<div class="card-sublinks">
|
||||||
<a href="lehrpfad_2_oop_einstieg/04_str_und_darstellung/aufgabe.py" download>📄 aufgabe.py</a>
|
<a href="lehrpfad_2_oop_einstieg/04_str_und_darstellung/aufgabe.py" download>📄 aufgabe.py</a>
|
||||||
|
|
@ -552,7 +575,8 @@
|
||||||
<div class="roadmap-desc">Vom Schul-IDLE zu einer modernen Entwicklerumgebung: Installation, Python-Extension & Terminal.</div>
|
<div class="roadmap-desc">Vom Schul-IDLE zu einer modernen Entwicklerumgebung: Installation, Python-Extension & Terminal.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<a href="lehrpfad_2_oop_einstieg/04b_umstieg_vscode/index.html" class="card-primary-btn" style="background: #4f46e5;">📖 Kapitel öffnen →</a>
|
<a href="workspace.html?track=lehrpfad_2_oop_einstieg&chapter=04b_umstieg_vscode" class="card-primary-btn" style="background: #4f46e5;">💻 In Web-IDE öffnen →</a>
|
||||||
|
<a href="lehrpfad_2_oop_einstieg/04b_umstieg_vscode/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
|
||||||
|
|
||||||
<div class="card-sublinks">
|
<div class="card-sublinks">
|
||||||
<a href="lehrpfad_2_oop_einstieg/04b_umstieg_vscode/vscode_test.py" download>💻 vscode_test.py</a>
|
<a href="lehrpfad_2_oop_einstieg/04b_umstieg_vscode/vscode_test.py" download>💻 vscode_test.py</a>
|
||||||
|
|
@ -567,7 +591,8 @@
|
||||||
<div class="roadmap-desc">Versionskontrolle: Commits, Branches, Status und kollaboratives Arbeiten im Terminal.</div>
|
<div class="roadmap-desc">Versionskontrolle: Commits, Branches, Status und kollaboratives Arbeiten im Terminal.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<a href="lehrpfad_2_oop_einstieg/04c_git_und_versionskontrolle/index.html" class="card-primary-btn" style="background: #4f46e5;">📖 Kapitel öffnen →</a>
|
<a href="workspace.html?track=lehrpfad_2_oop_einstieg&chapter=04c_git_und_versionskontrolle" class="card-primary-btn" style="background: #4f46e5;">💻 In Web-IDE öffnen →</a>
|
||||||
|
<a href="lehrpfad_2_oop_einstieg/04c_git_und_versionskontrolle/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
|
||||||
|
|
||||||
<div class="card-sublinks">
|
<div class="card-sublinks">
|
||||||
<a href="lehrpfad_2_oop_einstieg/04c_git_und_versionskontrolle/git_uebung.py" download>🌿 git_uebung.py</a>
|
<a href="lehrpfad_2_oop_einstieg/04c_git_und_versionskontrolle/git_uebung.py" download>🌿 git_uebung.py</a>
|
||||||
|
|
@ -582,7 +607,8 @@
|
||||||
<div class="roadmap-desc">Klassen interagieren lassen: Eine Playlist besitzt mehrere Songs, ein Warenkorb mehrere Artikel.</div>
|
<div class="roadmap-desc">Klassen interagieren lassen: Eine Playlist besitzt mehrere Songs, ein Warenkorb mehrere Artikel.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<a href="lehrpfad_2_oop_einstieg/05_objekte_kombinieren/index.html" class="card-primary-btn" style="background: #4f46e5;">📖 Kapitel öffnen →</a>
|
<a href="workspace.html?track=lehrpfad_2_oop_einstieg&chapter=05_objekte_kombinieren" class="card-primary-btn" style="background: #4f46e5;">💻 In Web-IDE öffnen →</a>
|
||||||
|
<a href="lehrpfad_2_oop_einstieg/05_objekte_kombinieren/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
|
||||||
|
|
||||||
<div class="card-sublinks">
|
<div class="card-sublinks">
|
||||||
<a href="lehrpfad_2_oop_einstieg/05_objekte_kombinieren/aufgabe.py" download>📄 aufgabe.py</a>
|
<a href="lehrpfad_2_oop_einstieg/05_objekte_kombinieren/aufgabe.py" download>📄 aufgabe.py</a>
|
||||||
|
|
@ -598,7 +624,8 @@
|
||||||
<div class="roadmap-desc">Das große interaktive Terminal-Haustier: Hunger, Energie, Glück, Schlafen, Füttern & Spielen.</div>
|
<div class="roadmap-desc">Das große interaktive Terminal-Haustier: Hunger, Energie, Glück, Schlafen, Füttern & Spielen.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<a href="lehrpfad_2_oop_einstieg/06_abschlussprojekt_tamagotchi/index.html" class="card-primary-btn" style="background: #4f46e5;">📖 Kapitel öffnen →</a>
|
<a href="workspace.html?track=lehrpfad_2_oop_einstieg&chapter=06_abschlussprojekt_tamagotchi" class="card-primary-btn" style="background: #4f46e5;">💻 In Web-IDE öffnen →</a>
|
||||||
|
<a href="lehrpfad_2_oop_einstieg/06_abschlussprojekt_tamagotchi/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
|
||||||
|
|
||||||
<div class="card-sublinks">
|
<div class="card-sublinks">
|
||||||
<a href="lehrpfad_2_oop_einstieg/06_abschlussprojekt_tamagotchi/aufgabe.py" download>📄 aufgabe.py</a>
|
<a href="lehrpfad_2_oop_einstieg/06_abschlussprojekt_tamagotchi/aufgabe.py" download>📄 aufgabe.py</a>
|
||||||
|
|
@ -607,20 +634,20 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- LEHRPFAD 3 -->
|
|
||||||
<section>
|
|
||||||
<div class="section-header">
|
|
||||||
<div>
|
|
||||||
<h2 id="lehrpfad-3">🚀 Lehrpfad 3: Fortgeschrittenes OOP & GUIs</h2>
|
|
||||||
<div class="section-desc">Dunder-Methoden, Unit Tests (TDD), Vererbung, Exceptions, JSON, Tkinter & Masterprojekt (07–16).</div>
|
|
||||||
</div>
|
</div>
|
||||||
<span class="track-badge track-3-badge">10 Module</span>
|
</section>
|
||||||
</div>
|
|
||||||
<div class="roadmap-grid">
|
<!-- LEHRPFAD 3 -->
|
||||||
|
<section>
|
||||||
|
<div class="section-header">
|
||||||
|
<div>
|
||||||
|
<h2 id="lehrpfad-3">🚀 Lehrpfad 3: Fortgeschrittenes OOP & GUIs</h2>
|
||||||
|
<div class="section-desc">Dunder-Methoden, Unit Tests (TDD), Vererbung, Exceptions, JSON, Tkinter & Masterprojekt (07–16).</div>
|
||||||
|
</div>
|
||||||
|
<span class="track-badge track-3-badge">10 Module</span>
|
||||||
|
</div>
|
||||||
|
<div class="roadmap-grid">
|
||||||
|
|
||||||
<div class="roadmap-card" style="border-left: 4px solid #7c3aed;">
|
<div class="roadmap-card" style="border-left: 4px solid #7c3aed;">
|
||||||
<div>
|
<div>
|
||||||
<span class="roadmap-number" style="background: #7c3aed18; color: #7c3aed;">Adv 07</span>
|
<span class="roadmap-number" style="background: #7c3aed18; color: #7c3aed;">Adv 07</span>
|
||||||
|
|
@ -628,7 +655,8 @@
|
||||||
<div class="roadmap-desc">Veränderliche vs. unveränderliche Datentypen, <code>is</code> vs. <code>==</code>, flache und tiefe Kopien.</div>
|
<div class="roadmap-desc">Veränderliche vs. unveränderliche Datentypen, <code>is</code> vs. <code>==</code>, flache und tiefe Kopien.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<a href="lehrpfad_3_fortgeschrittenes_oop/07_referenzen_und_speicher/index.html" class="card-primary-btn" style="background: #7c3aed;">📖 Kapitel öffnen →</a>
|
<a href="workspace.html?track=lehrpfad_3_fortgeschrittenes_oop&chapter=07_referenzen_und_speicher" class="card-primary-btn" style="background: #7c3aed;">💻 In Web-IDE öffnen →</a>
|
||||||
|
<a href="lehrpfad_3_fortgeschrittenes_oop/07_referenzen_und_speicher/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
|
||||||
|
|
||||||
<div class="card-sublinks">
|
<div class="card-sublinks">
|
||||||
<a href="lehrpfad_3_fortgeschrittenes_oop/07_referenzen_und_speicher/aufgabe.py" download>📄 aufgabe.py</a>
|
<a href="lehrpfad_3_fortgeschrittenes_oop/07_referenzen_und_speicher/aufgabe.py" download>📄 aufgabe.py</a>
|
||||||
|
|
@ -644,7 +672,8 @@
|
||||||
<div class="roadmap-desc">Klassen rechen- und vergleichbar machen mit <code>__add__</code>, <code>__eq__</code>, <code>__len__</code>.</div>
|
<div class="roadmap-desc">Klassen rechen- und vergleichbar machen mit <code>__add__</code>, <code>__eq__</code>, <code>__len__</code>.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<a href="lehrpfad_3_fortgeschrittenes_oop/08_operator_overloading_dunder/index.html" class="card-primary-btn" style="background: #7c3aed;">📖 Kapitel öffnen →</a>
|
<a href="workspace.html?track=lehrpfad_3_fortgeschrittenes_oop&chapter=08_operator_overloading_dunder" class="card-primary-btn" style="background: #7c3aed;">💻 In Web-IDE öffnen →</a>
|
||||||
|
<a href="lehrpfad_3_fortgeschrittenes_oop/08_operator_overloading_dunder/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
|
||||||
|
|
||||||
<div class="card-sublinks">
|
<div class="card-sublinks">
|
||||||
<a href="lehrpfad_3_fortgeschrittenes_oop/08_operator_overloading_dunder/aufgabe.py" download>📄 aufgabe.py</a>
|
<a href="lehrpfad_3_fortgeschrittenes_oop/08_operator_overloading_dunder/aufgabe.py" download>📄 aufgabe.py</a>
|
||||||
|
|
@ -660,7 +689,8 @@
|
||||||
<div class="roadmap-desc">Software-Qualität sichern: Eigene Testsuiten mit <code>unittest</code> schreiben (Test-Driven Development).</div>
|
<div class="roadmap-desc">Software-Qualität sichern: Eigene Testsuiten mit <code>unittest</code> schreiben (Test-Driven Development).</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<a href="lehrpfad_3_fortgeschrittenes_oop/09_eigene_unit_tests_schreiben/index.html" class="card-primary-btn" style="background: #7c3aed;">📖 Kapitel öffnen →</a>
|
<a href="workspace.html?track=lehrpfad_3_fortgeschrittenes_oop&chapter=09_eigene_unit_tests_schreiben" class="card-primary-btn" style="background: #7c3aed;">💻 In Web-IDE öffnen →</a>
|
||||||
|
<a href="lehrpfad_3_fortgeschrittenes_oop/09_eigene_unit_tests_schreiben/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
|
||||||
|
|
||||||
<div class="card-sublinks">
|
<div class="card-sublinks">
|
||||||
<a href="lehrpfad_3_fortgeschrittenes_oop/09_eigene_unit_tests_schreiben/aufgabe.py" download>📄 aufgabe.py</a>
|
<a href="lehrpfad_3_fortgeschrittenes_oop/09_eigene_unit_tests_schreiben/aufgabe.py" download>📄 aufgabe.py</a>
|
||||||
|
|
@ -676,7 +706,8 @@
|
||||||
<div class="roadmap-desc">Code wiederverwenden nach dem DRY-Prinzip: Basisklassen und abgeleitete Kindklassen.</div>
|
<div class="roadmap-desc">Code wiederverwenden nach dem DRY-Prinzip: Basisklassen und abgeleitete Kindklassen.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<a href="lehrpfad_3_fortgeschrittenes_oop/10_vererbung_und_super/index.html" class="card-primary-btn" style="background: #7c3aed;">📖 Kapitel öffnen →</a>
|
<a href="workspace.html?track=lehrpfad_3_fortgeschrittenes_oop&chapter=10_vererbung_und_super" class="card-primary-btn" style="background: #7c3aed;">💻 In Web-IDE öffnen →</a>
|
||||||
|
<a href="lehrpfad_3_fortgeschrittenes_oop/10_vererbung_und_super/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
|
||||||
|
|
||||||
<div class="card-sublinks">
|
<div class="card-sublinks">
|
||||||
<a href="lehrpfad_3_fortgeschrittenes_oop/10_vererbung_und_super/aufgabe.py" download>📄 aufgabe.py</a>
|
<a href="lehrpfad_3_fortgeschrittenes_oop/10_vererbung_und_super/aufgabe.py" download>📄 aufgabe.py</a>
|
||||||
|
|
@ -692,7 +723,8 @@
|
||||||
<div class="roadmap-desc">Vielgestaltigkeit: Gleiche Methodennamen bei unterschiedlichen Klassen (Duck Typing).</div>
|
<div class="roadmap-desc">Vielgestaltigkeit: Gleiche Methodennamen bei unterschiedlichen Klassen (Duck Typing).</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<a href="lehrpfad_3_fortgeschrittenes_oop/11_polymorphie_und_interfaces/index.html" class="card-primary-btn" style="background: #7c3aed;">📖 Kapitel öffnen →</a>
|
<a href="workspace.html?track=lehrpfad_3_fortgeschrittenes_oop&chapter=11_polymorphie_und_interfaces" class="card-primary-btn" style="background: #7c3aed;">💻 In Web-IDE öffnen →</a>
|
||||||
|
<a href="lehrpfad_3_fortgeschrittenes_oop/11_polymorphie_und_interfaces/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
|
||||||
|
|
||||||
<div class="card-sublinks">
|
<div class="card-sublinks">
|
||||||
<a href="lehrpfad_3_fortgeschrittenes_oop/11_polymorphie_und_interfaces/aufgabe.py" download>📄 aufgabe.py</a>
|
<a href="lehrpfad_3_fortgeschrittenes_oop/11_polymorphie_und_interfaces/aufgabe.py" download>📄 aufgabe.py</a>
|
||||||
|
|
@ -708,7 +740,8 @@
|
||||||
<div class="roadmap-desc">Fehler abfangen mit <code>try/except/finally</code> und eigene Exception-Klassen definieren.</div>
|
<div class="roadmap-desc">Fehler abfangen mit <code>try/except/finally</code> und eigene Exception-Klassen definieren.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<a href="lehrpfad_3_fortgeschrittenes_oop/12_exceptions_und_fehlerbehandlung/index.html" class="card-primary-btn" style="background: #7c3aed;">📖 Kapitel öffnen →</a>
|
<a href="workspace.html?track=lehrpfad_3_fortgeschrittenes_oop&chapter=12_exceptions_und_fehlerbehandlung" class="card-primary-btn" style="background: #7c3aed;">💻 In Web-IDE öffnen →</a>
|
||||||
|
<a href="lehrpfad_3_fortgeschrittenes_oop/12_exceptions_und_fehlerbehandlung/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
|
||||||
|
|
||||||
<div class="card-sublinks">
|
<div class="card-sublinks">
|
||||||
<a href="lehrpfad_3_fortgeschrittenes_oop/12_exceptions_und_fehlerbehandlung/aufgabe.py" download>📄 aufgabe.py</a>
|
<a href="lehrpfad_3_fortgeschrittenes_oop/12_exceptions_und_fehlerbehandlung/aufgabe.py" download>📄 aufgabe.py</a>
|
||||||
|
|
@ -724,7 +757,8 @@
|
||||||
<div class="roadmap-desc">Daten dauerhaft speichern und laden: Savegames, Konfigurationen und Tabellen-Export.</div>
|
<div class="roadmap-desc">Daten dauerhaft speichern und laden: Savegames, Konfigurationen und Tabellen-Export.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<a href="lehrpfad_3_fortgeschrittenes_oop/13_persistenz_json_und_csv/index.html" class="card-primary-btn" style="background: #7c3aed;">📖 Kapitel öffnen →</a>
|
<a href="workspace.html?track=lehrpfad_3_fortgeschrittenes_oop&chapter=13_persistenz_json_und_csv" class="card-primary-btn" style="background: #7c3aed;">💻 In Web-IDE öffnen →</a>
|
||||||
|
<a href="lehrpfad_3_fortgeschrittenes_oop/13_persistenz_json_und_csv/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
|
||||||
|
|
||||||
<div class="card-sublinks">
|
<div class="card-sublinks">
|
||||||
<a href="lehrpfad_3_fortgeschrittenes_oop/13_persistenz_json_und_csv/aufgabe.py" download>📄 aufgabe.py</a>
|
<a href="lehrpfad_3_fortgeschrittenes_oop/13_persistenz_json_und_csv/aufgabe.py" download>📄 aufgabe.py</a>
|
||||||
|
|
@ -740,7 +774,8 @@
|
||||||
<div class="roadmap-desc">Grafische Benutzeroberflächen: Fenster, Buttons, Eingabefelder und Event-Handler.</div>
|
<div class="roadmap-desc">Grafische Benutzeroberflächen: Fenster, Buttons, Eingabefelder und Event-Handler.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<a href="lehrpfad_3_fortgeschrittenes_oop/14_gui_mit_tkinter/index.html" class="card-primary-btn" style="background: #7c3aed;">📖 Kapitel öffnen →</a>
|
<a href="workspace.html?track=lehrpfad_3_fortgeschrittenes_oop&chapter=14_gui_mit_tkinter" class="card-primary-btn" style="background: #7c3aed;">💻 In Web-IDE öffnen →</a>
|
||||||
|
<a href="lehrpfad_3_fortgeschrittenes_oop/14_gui_mit_tkinter/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
|
||||||
|
|
||||||
<div class="card-sublinks">
|
<div class="card-sublinks">
|
||||||
<a href="lehrpfad_3_fortgeschrittenes_oop/14_gui_mit_tkinter/aufgabe.py" download>📄 aufgabe.py</a>
|
<a href="lehrpfad_3_fortgeschrittenes_oop/14_gui_mit_tkinter/aufgabe.py" download>📄 aufgabe.py</a>
|
||||||
|
|
@ -756,7 +791,8 @@
|
||||||
<div class="roadmap-desc">Variable Argumente, Unpacking und eigene Container-Klassen mit <code>__getitem__</code>.</div>
|
<div class="roadmap-desc">Variable Argumente, Unpacking und eigene Container-Klassen mit <code>__getitem__</code>.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<a href="lehrpfad_3_fortgeschrittenes_oop/15_parameter_und_container/index.html" class="card-primary-btn" style="background: #7c3aed;">📖 Kapitel öffnen →</a>
|
<a href="workspace.html?track=lehrpfad_3_fortgeschrittenes_oop&chapter=15_parameter_und_container" class="card-primary-btn" style="background: #7c3aed;">💻 In Web-IDE öffnen →</a>
|
||||||
|
<a href="lehrpfad_3_fortgeschrittenes_oop/15_parameter_und_container/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
|
||||||
|
|
||||||
<div class="card-sublinks">
|
<div class="card-sublinks">
|
||||||
<a href="lehrpfad_3_fortgeschrittenes_oop/15_parameter_und_container/aufgabe.py" download>📄 aufgabe.py</a>
|
<a href="lehrpfad_3_fortgeschrittenes_oop/15_parameter_und_container/aufgabe.py" download>📄 aufgabe.py</a>
|
||||||
|
|
@ -772,7 +808,8 @@
|
||||||
<div class="roadmap-desc">Die Königsdisziplin: Eine vollständige Desktop-App mit MVC, Persistenz, Tests und GUI.</div>
|
<div class="roadmap-desc">Die Königsdisziplin: Eine vollständige Desktop-App mit MVC, Persistenz, Tests und GUI.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<a href="lehrpfad_3_fortgeschrittenes_oop/16_master_abschlussprojekt/index.html" class="card-primary-btn" style="background: #7c3aed;">📖 Kapitel öffnen →</a>
|
<a href="workspace.html?track=lehrpfad_3_fortgeschrittenes_oop&chapter=16_master_abschlussprojekt" class="card-primary-btn" style="background: #7c3aed;">💻 In Web-IDE öffnen →</a>
|
||||||
|
<a href="lehrpfad_3_fortgeschrittenes_oop/16_master_abschlussprojekt/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
|
||||||
|
|
||||||
<div class="card-sublinks">
|
<div class="card-sublinks">
|
||||||
<a href="lehrpfad_3_fortgeschrittenes_oop/16_master_abschlussprojekt/aufgabe.py" download>📄 aufgabe.py</a>
|
<a href="lehrpfad_3_fortgeschrittenes_oop/16_master_abschlussprojekt/aufgabe.py" download>📄 aufgabe.py</a>
|
||||||
|
|
@ -781,70 +818,70 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 🎮 Gamification Dashboard -->
|
<!-- 🎮 Gamification Dashboard -->
|
||||||
<section class="hud-card" id="gamification-hud">
|
<section class="hud-card" id="gamification-hud">
|
||||||
<div class="hud-header">
|
<div class="hud-header">
|
||||||
<div class="hud-user">
|
<div class="hud-user">
|
||||||
<div class="hud-avatar" id="hud-avatar-icon">🐣</div>
|
<div class="hud-avatar" id="hud-avatar-icon">🐣</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="hud-name" id="hud-user-name">Entwickler-Pass</div>
|
<div class="hud-name" id="hud-user-name">Entwickler-Pass</div>
|
||||||
<div class="hud-title"><span id="hud-user-title">Code-Küken</span> • <span class="hud-rank" id="hud-user-rank">Rang: Bronze</span></div>
|
<div class="hud-title"><span id="hud-user-title">Code-Küken</span> • <span class="hud-rank" id="hud-user-rank">Rang: Bronze</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hud-stats">
|
||||||
|
<div class="hud-stat-box">
|
||||||
|
<span class="hud-stat-value" id="hud-stat-level">1</span>
|
||||||
|
<span class="hud-stat-label">Level</span>
|
||||||
|
</div>
|
||||||
|
<div class="hud-stat-box">
|
||||||
|
<span class="hud-stat-value" id="hud-stat-xp">0 XP</span>
|
||||||
|
<span class="hud-stat-label">Erfahrung</span>
|
||||||
|
</div>
|
||||||
|
<div class="hud-stat-box">
|
||||||
|
<span class="hud-stat-value" id="hud-stat-solved">0 / 27</span>
|
||||||
|
<span class="hud-stat-label">Gelöst</span>
|
||||||
|
</div>
|
||||||
|
<div class="hud-stat-box">
|
||||||
|
<span class="hud-stat-value" id="hud-stat-streak">🔥 1 Tag</span>
|
||||||
|
<span class="hud-stat-label">Streak</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="hud-stats">
|
|
||||||
<div class="hud-stat-box">
|
|
||||||
<span class="hud-stat-value" id="hud-stat-level">1</span>
|
|
||||||
<span class="hud-stat-label">Level</span>
|
|
||||||
</div>
|
|
||||||
<div class="hud-stat-box">
|
|
||||||
<span class="hud-stat-value" id="hud-stat-xp">0 XP</span>
|
|
||||||
<span class="hud-stat-label">Erfahrung</span>
|
|
||||||
</div>
|
|
||||||
<div class="hud-stat-box">
|
|
||||||
<span class="hud-stat-value" id="hud-stat-solved">0 / 27</span>
|
|
||||||
<span class="hud-stat-label">Gelöst</span>
|
|
||||||
</div>
|
|
||||||
<div class="hud-stat-box">
|
|
||||||
<span class="hud-stat-value" id="hud-stat-streak">🔥 1 Tag</span>
|
|
||||||
<span class="hud-stat-label">Streak</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hud-progress-container">
|
<div class="hud-progress-container">
|
||||||
<div class="hud-progress-labels">
|
<div class="hud-progress-labels">
|
||||||
<span>Fortschritt zum nächsten Level</span>
|
<span>Fortschritt zum nächsten Level</span>
|
||||||
<span id="hud-progress-text">0 / 100 XP</span>
|
<span id="hud-progress-text">0 / 100 XP</span>
|
||||||
|
</div>
|
||||||
|
<div class="hud-progress-bar-bg">
|
||||||
|
<div class="hud-progress-bar-fill" id="hud-progress-bar" style="width: 0%;"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="hud-progress-bar-bg">
|
|
||||||
<div class="hud-progress-bar-fill" id="hud-progress-bar" style="width: 0%;"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hud-footer">
|
<div class="hud-footer">
|
||||||
<div class="hud-cli-tip">
|
<div class="hud-cli-tip">
|
||||||
💻 Terminal-Befehl: <code>python3 profil.py</code> • Gesamttest: <code>python3 test_all.py</code>
|
💻 Terminal-Befehl: <code>python3 profil.py</code> • Gesamttest: <code>python3 test_all.py</code>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="hud-btn" onclick="if(window.renderGamificationHUD) window.renderGamificationHUD();">
|
||||||
|
🔄 Status aktualisieren
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="hud-btn" onclick="if(window.renderGamificationHUD) window.renderGamificationHUD();">
|
</section>
|
||||||
🔄 Status aktualisieren
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- ⚡ Live Python Playground -->
|
<!-- ⚡ Live Python Playground -->
|
||||||
<section class="card" id="python-live-playground">
|
<section class="card" id="python-live-playground">
|
||||||
<h2>⚡ Interaktiver Python Live-Playground (Pyodide WebAssembly)</h2>
|
<h2>⚡ Interaktiver Python Live-Playground (Pyodide WebAssembly)</h2>
|
||||||
<p>Führe beliebigen Python-Code direkt im Browser aus – ohne Installation!</p>
|
<p>Führe beliebigen Python-Code direkt im Browser aus – ohne Installation!</p>
|
||||||
|
|
||||||
<div class="code-container">
|
<div class="code-container">
|
||||||
<div class="code-header">
|
<div class="code-header">
|
||||||
<span>🐍 Python Code-Editor</span>
|
<span>🐍 Python Code-Editor</span>
|
||||||
<button type="button" class="btn" id="btn-run-code" style="padding: 4px 12px; font-size: 0.8rem;">▶ Code ausführen</button>
|
<button type="button" class="btn" id="btn-run-code" style="padding: 4px 12px; font-size: 0.8rem;">▶ Code ausführen</button>
|
||||||
</div>
|
</div>
|
||||||
<textarea id="playground-editor" style="width: 100%; height: 160px; background: #0b0f19; color: #f8fafc; font-family: var(--font-mono); font-size: 0.95rem; border: none; padding: 14px; outline: none; resize: vertical;"># Schreibe hier deinen Python-Code:
|
<textarea id="playground-editor" style="width: 100%; height: 160px; background: #0b0f19; color: #f8fafc; font-family: var(--font-mono); font-size: 0.95rem; border: none; padding: 14px; outline: none; resize: vertical;"># Schreibe hier deinen Python-Code:
|
||||||
class Auto:
|
class Auto:
|
||||||
def __init__(self, marke, ps):
|
def __init__(self, marke, ps):
|
||||||
self.marke = marke
|
self.marke = marke
|
||||||
|
|
@ -856,44 +893,44 @@ class Auto:
|
||||||
mein_auto = Auto("Porsche 911", 450)
|
mein_auto = Auto("Porsche 911", 450)
|
||||||
print(mein_auto.hupen())
|
print(mein_auto.hupen())
|
||||||
</textarea>
|
</textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="background: #060911; border-radius: var(--radius-sm); border: 1px solid var(--border-color); padding: 14px; margin-top: 10px;">
|
<div style="background: #060911; border-radius: var(--radius-sm); border: 1px solid var(--border-color); padding: 14px; margin-top: 10px;">
|
||||||
<div style="font-size: 0.8rem; font-weight: 700; color: #94a3b8; margin-bottom: 6px;">🖥️ Programmausgabe (Terminal):</div>
|
<div style="font-size: 0.8rem; font-weight: 700; color: #94a3b8; margin-bottom: 6px;">🖥️ Programmausgabe (Terminal):</div>
|
||||||
<pre id="playground-output" style="color: #38bdf8; min-height: 50px;">Klicke oben auf '▶ Code ausführen', um das Skript zu starten...</pre>
|
<pre id="playground-output" style="color: #38bdf8; min-height: 50px;">Klicke oben auf '▶ Code ausführen', um das Skript zu starten...</pre>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 🩺 Fehler-Dolmetscher -->
|
<!-- 🩺 Fehler-Dolmetscher -->
|
||||||
<section class="card" id="error-interpreter-widget">
|
<section class="card" id="error-interpreter-widget">
|
||||||
<h2>🩺 Interaktiver Python Fehler-Dolmetscher</h2>
|
<h2>🩺 Interaktiver Python Fehler-Dolmetscher</h2>
|
||||||
<p>Füge eine englische Fehlermeldung (Traceback) aus dem Terminal ein:</p>
|
<p>Füge eine englische Fehlermeldung (Traceback) aus dem Terminal ein:</p>
|
||||||
|
|
||||||
<textarea id="error-input" placeholder="Beispiel: TypeError: unsupported operand type(s) for +: 'int' and 'str'" style="width: 100%; height: 90px; border: 1px solid var(--border-color); border-radius: var(--radius-sm); padding: 10px; font-family: var(--font-mono); font-size: 0.9rem; margin-bottom: 10px;"></textarea>
|
<textarea id="error-input" placeholder="Beispiel: TypeError: unsupported operand type(s) for +: 'int' and 'str'" style="width: 100%; height: 90px; border: 1px solid var(--border-color); border-radius: var(--radius-sm); padding: 10px; font-family: var(--font-mono); font-size: 0.9rem; margin-bottom: 10px;"></textarea>
|
||||||
<button type="button" class="btn" id="btn-interpret-error">🔍 Fehler übersetzen & erklären</button>
|
<button type="button" class="btn" id="btn-interpret-error">🔍 Fehler übersetzen & erklären</button>
|
||||||
|
|
||||||
<div id="error-result" style="display: none; margin-top: 15px; padding: 15px; border-radius: var(--radius-sm); background: #f0fdf4; border: 1px solid #bbf7d0;">
|
<div id="error-result" style="display: none; margin-top: 15px; padding: 15px; border-radius: var(--radius-sm); background: #f0fdf4; border: 1px solid #bbf7d0;">
|
||||||
<h4 id="error-name" style="margin-top: 0; color: #166534;"></h4>
|
<h4 id="error-name" style="margin-top: 0; color: #166534;"></h4>
|
||||||
<p id="error-explanation" style="color: #1e293b; margin: 8px 0;"></p>
|
<p id="error-explanation" style="color: #1e293b; margin: 8px 0;"></p>
|
||||||
<div style="font-weight: 700; color: #166534; margin-top: 8px;">🛠️ So behebst du das Problem:</div>
|
<div style="font-weight: 700; color: #166534; margin-top: 8px;">🛠️ So behebst du das Problem:</div>
|
||||||
<ul id="error-fixes" style="padding-left: 20px; color: #334155; margin-top: 4px;"></ul>
|
<ul id="error-fixes" style="padding-left: 20px; color: #334155; margin-top: 4px;"></ul>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 🏆 Zertifikate -->
|
<!-- 🏆 Zertifikate -->
|
||||||
<section class="card" id="zertifikate">
|
<section class="card" id="zertifikate">
|
||||||
<h2>🏆 Lehrpfad-Abschluss-Zertifikate</h2>
|
<h2>🏆 Lehrpfad-Abschluss-Zertifikate</h2>
|
||||||
<p>Generiere offizielle, druckbare Zertifikate nach erfolgreichem Abschluss eines Lehrpfads:</p>
|
<p>Generiere offizielle, druckbare Zertifikate nach erfolgreichem Abschluss eines Lehrpfads:</p>
|
||||||
|
|
||||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; margin-top: 14px;">
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; margin-top: 14px;">
|
||||||
<button type="button" class="btn" onclick="if(window.openCertificate) window.openCertificate('lehrpfad_1');">📜 Zertifikat Lehrpfad 1</button>
|
<button type="button" class="btn" onclick="if(window.openCertificate) window.openCertificate('lehrpfad_1');">📜 Zertifikat Lehrpfad 1</button>
|
||||||
<button type="button" class="btn" onclick="if(window.openCertificate) window.openCertificate('lehrpfad_2');">📜 Zertifikat Lehrpfad 2</button>
|
<button type="button" class="btn" onclick="if(window.openCertificate) window.openCertificate('lehrpfad_2');">📜 Zertifikat Lehrpfad 2</button>
|
||||||
<button type="button" class="btn" onclick="if(window.openCertificate) window.openCertificate('lehrpfad_3');">📜 Zertifikat Lehrpfad 3</button>
|
<button type="button" class="btn" onclick="if(window.openCertificate) window.openCertificate('lehrpfad_3');">📜 Zertifikat Lehrpfad 3</button>
|
||||||
<button type="button" class="btn" onclick="if(window.openCertificate) window.openCertificate('master');">👑 Master-Zertifikat</button>
|
<button type="button" class="btn" onclick="if(window.openCertificate) window.openCertificate('master');">👑 Master-Zertifikat</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -908,6 +945,7 @@ print(mein_auto.hupen())
|
||||||
<script src="assets/parsons.js"></script>
|
<script src="assets/parsons.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/sidebar.js"></script>
|
<script src="assets/sidebar.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
486
server.py
Normal file
486
server.py
Normal file
|
|
@ -0,0 +1,486 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
🐍 PYTHON LERNPLATTFORM – BACKEND SERVER 🐍
|
||||||
|
===========================================
|
||||||
|
Leichtgewichtiger, abhängigkeitsfreier REST-API & Static Server für die Online-Lernplattform.
|
||||||
|
- Läuft auf jedem Linux-Server / Webspace mit Standard-Python 3.
|
||||||
|
- SQLite-Datenbank (User-Accounts, Code-Drafts, XP, Klassen & Lehrer-Dashboard).
|
||||||
|
- PBKDF2-HMAC-SHA256 Passwort-Verschlüsselung & signierte Session-Tokens.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import http.server
|
||||||
|
import socketserver
|
||||||
|
import json
|
||||||
|
import sqlite3
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
import secrets
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
import urllib.parse
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
PORT = int(os.environ.get("PORT", 8000))
|
||||||
|
BASE_DIR = Path(__file__).parent.resolve()
|
||||||
|
DB_FILE = BASE_DIR / "platform_data.db"
|
||||||
|
SECRET_KEY = secrets.token_hex(32)
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# DATENBANK INITIALISIERUNG
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
conn = sqlite3.connect(str(DB_FILE))
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
return conn
|
||||||
|
|
||||||
|
def init_db():
|
||||||
|
conn = get_db()
|
||||||
|
c = conn.cursor()
|
||||||
|
|
||||||
|
# 1. Benutzer-Tabelle
|
||||||
|
c.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
email TEXT UNIQUE NOT NULL,
|
||||||
|
password_hash TEXT NOT NULL,
|
||||||
|
salt TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
role TEXT DEFAULT 'student',
|
||||||
|
xp INTEGER DEFAULT 0,
|
||||||
|
level INTEGER DEFAULT 1,
|
||||||
|
streak_days INTEGER DEFAULT 1,
|
||||||
|
last_active_date TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)""")
|
||||||
|
|
||||||
|
# 2. Kapitel-Fortschritt & Code-Drafts
|
||||||
|
c.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS chapter_progress (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
chapter_id TEXT NOT NULL,
|
||||||
|
code_draft TEXT,
|
||||||
|
subgoals_json TEXT,
|
||||||
|
is_solved INTEGER DEFAULT 0,
|
||||||
|
solved_at TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE(user_id, chapter_id),
|
||||||
|
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
)""")
|
||||||
|
|
||||||
|
# 3. Klassenräume (Lehrer-Feature)
|
||||||
|
c.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS classrooms (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
teacher_id INTEGER NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
invite_code TEXT UNIQUE NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY(teacher_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
)""")
|
||||||
|
|
||||||
|
# 4. Klassen-Mitgliedschaften
|
||||||
|
c.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS class_enrollments (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
classroom_id INTEGER NOT NULL,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE(classroom_id, user_id),
|
||||||
|
FOREIGN KEY(classroom_id) REFERENCES classrooms(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
)""")
|
||||||
|
|
||||||
|
# 5. Freigeschaltete Trophäen
|
||||||
|
c.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS achievements (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
trophy_id TEXT NOT NULL,
|
||||||
|
unlocked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE(user_id, trophy_id),
|
||||||
|
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
)""")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# AUTHENTIFIZIERUNG & HASHING
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
def hash_password(password: str, salt: str = None) -> tuple[str, str]:
|
||||||
|
if not salt:
|
||||||
|
salt = secrets.token_hex(16)
|
||||||
|
pwd_bytes = password.encode("utf-8")
|
||||||
|
salt_bytes = salt.encode("utf-8")
|
||||||
|
key = hashlib.pbkdf2_hmac("sha256", pwd_bytes, salt_bytes, 100000)
|
||||||
|
return key.hex(), salt
|
||||||
|
|
||||||
|
def verify_password(password: str, password_hash: str, salt: str) -> bool:
|
||||||
|
new_hash, _ = hash_password(password, salt)
|
||||||
|
return hmac.compare_digest(new_hash, password_hash)
|
||||||
|
|
||||||
|
def generate_token(user_id: int, email: str, role: str) -> str:
|
||||||
|
payload = {
|
||||||
|
"uid": user_id,
|
||||||
|
"email": email,
|
||||||
|
"role": role,
|
||||||
|
"exp": int(time.time()) + (86400 * 30) # 30 Tage gültig
|
||||||
|
}
|
||||||
|
payload_json = json.dumps(payload)
|
||||||
|
sig = hmac.new(SECRET_KEY.encode("utf-8"), payload_json.encode("utf-8"), hashlib.sha256).hexdigest()
|
||||||
|
token = f"{payload_json}.{sig}"
|
||||||
|
return urllib.parse.quote(token)
|
||||||
|
|
||||||
|
def verify_token(token_str: str) -> dict | None:
|
||||||
|
try:
|
||||||
|
raw_token = urllib.parse.unquote(token_str)
|
||||||
|
if "." not in raw_token:
|
||||||
|
return None
|
||||||
|
payload_json, sig = raw_token.rsplit(".", 1)
|
||||||
|
expected_sig = hmac.new(SECRET_KEY.encode("utf-8"), payload_json.encode("utf-8"), hashlib.sha256).hexdigest()
|
||||||
|
if not hmac.compare_digest(sig, expected_sig):
|
||||||
|
return None
|
||||||
|
payload = json.loads(payload_json)
|
||||||
|
if payload.get("exp", 0) < time.time():
|
||||||
|
return None
|
||||||
|
return payload
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# HTTP REQUEST HANDLER
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
class PlatformRequestHandler(http.server.SimpleHTTPRequestHandler):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, directory=str(BASE_DIR), **kwargs)
|
||||||
|
|
||||||
|
def end_headers(self):
|
||||||
|
# CORS & Cache-Header für moderne Web-Apps
|
||||||
|
self.send_header("Access-Control-Allow-Origin", "*")
|
||||||
|
self.send_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||||
|
self.send_header("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||||
|
self.send_header("Cache-Control", "no-cache, must-revalidate")
|
||||||
|
super().end_headers()
|
||||||
|
|
||||||
|
def do_OPTIONS(self):
|
||||||
|
self.send_response(204)
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
def send_json(self, data: dict, status: int = 200):
|
||||||
|
self.send_response(status)
|
||||||
|
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(json.dumps(data, ensure_ascii=False).encode("utf-8"))
|
||||||
|
|
||||||
|
def get_auth_user(self) -> dict | None:
|
||||||
|
auth_header = self.headers.get("Authorization", "")
|
||||||
|
if auth_header.startswith("Bearer "):
|
||||||
|
token = auth_header[7:].strip()
|
||||||
|
return verify_token(token)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def read_json_body(self) -> dict:
|
||||||
|
content_length = int(self.headers.get("Content-Length", 0))
|
||||||
|
if content_length == 0:
|
||||||
|
return {}
|
||||||
|
body = self.rfile.read(content_length)
|
||||||
|
return json.loads(body.decode("utf-8"))
|
||||||
|
|
||||||
|
def do_GET(self):
|
||||||
|
parsed = urllib.parse.urlparse(self.path)
|
||||||
|
path = parsed.path
|
||||||
|
|
||||||
|
# --- API ROUTES ---
|
||||||
|
if path == "/api/auth/me":
|
||||||
|
user_token = self.get_auth_user()
|
||||||
|
if not user_token:
|
||||||
|
return self.send_json({"error": "Nicht authentifiziert"}, 401)
|
||||||
|
conn = get_db()
|
||||||
|
row = conn.execute("SELECT id, email, name, role, xp, level, streak_days FROM users WHERE id = ?", (user_token["uid"],)).fetchone()
|
||||||
|
conn.close()
|
||||||
|
if not row:
|
||||||
|
return self.send_json({"error": "Benutzer nicht gefunden"}, 404)
|
||||||
|
return self.send_json({"user": dict(row)})
|
||||||
|
|
||||||
|
elif path.startswith("/api/progress/get"):
|
||||||
|
user_token = self.get_auth_user()
|
||||||
|
if not user_token:
|
||||||
|
return self.send_json({"error": "Nicht authentifiziert"}, 401)
|
||||||
|
query = urllib.parse.parse_qs(parsed.query)
|
||||||
|
chapter_id = query.get("chapter_id", [""])[0]
|
||||||
|
|
||||||
|
conn = get_db()
|
||||||
|
if chapter_id:
|
||||||
|
row = conn.execute("SELECT chapter_id, code_draft, subgoals_json, is_solved, solved_at FROM chapter_progress WHERE user_id = ? AND chapter_id = ?", (user_token["uid"], chapter_id)).fetchone()
|
||||||
|
conn.close()
|
||||||
|
return self.send_json({"progress": dict(row) if row else None})
|
||||||
|
else:
|
||||||
|
rows = conn.execute("SELECT chapter_id, is_solved, solved_at FROM chapter_progress WHERE user_id = ?", (user_token["uid"],)).fetchall()
|
||||||
|
conn.close()
|
||||||
|
return self.send_json({"all_progress": [dict(r) for r in rows]})
|
||||||
|
|
||||||
|
elif path.startswith("/api/classrooms/list"):
|
||||||
|
user_token = self.get_auth_user()
|
||||||
|
if not user_token:
|
||||||
|
return self.send_json({"error": "Nicht authentifiziert"}, 401)
|
||||||
|
|
||||||
|
conn = get_db()
|
||||||
|
if user_token.get("role") == "teacher":
|
||||||
|
classes = conn.execute("SELECT id, name, invite_code, created_at FROM classrooms WHERE teacher_id = ?", (user_token["uid"],)).fetchall()
|
||||||
|
result = []
|
||||||
|
for cl in classes:
|
||||||
|
student_count = conn.execute("SELECT COUNT(*) FROM class_enrollments WHERE classroom_id = ?", (cl["id"],)).fetchone()[0]
|
||||||
|
result.append({**dict(cl), "student_count": student_count})
|
||||||
|
conn.close()
|
||||||
|
return self.send_json({"classrooms": result})
|
||||||
|
else:
|
||||||
|
classes = conn.execute("""
|
||||||
|
SELECT c.id, c.name, u.name as teacher_name, ce.joined_at
|
||||||
|
FROM class_enrollments ce
|
||||||
|
JOIN classrooms c ON ce.classroom_id = c.id
|
||||||
|
JOIN users u ON c.teacher_id = u.id
|
||||||
|
WHERE ce.user_id = ?
|
||||||
|
""", (user_token["uid"],)).fetchall()
|
||||||
|
conn.close()
|
||||||
|
return self.send_json({"classrooms": [dict(c) for c in classes]})
|
||||||
|
|
||||||
|
elif path.startswith("/api/classrooms/matrix"):
|
||||||
|
user_token = self.get_auth_user()
|
||||||
|
if not user_token or user_token.get("role") != "teacher":
|
||||||
|
return self.send_json({"error": "Nur für Lehrkräfte"}, 403)
|
||||||
|
|
||||||
|
query = urllib.parse.parse_qs(parsed.query)
|
||||||
|
classroom_id = query.get("classroom_id", [""])[0]
|
||||||
|
if not classroom_id:
|
||||||
|
return self.send_json({"error": "classroom_id fehlt"}, 400)
|
||||||
|
|
||||||
|
conn = get_db()
|
||||||
|
students = conn.execute("""
|
||||||
|
SELECT u.id, u.name, u.email, u.xp, u.level, u.streak_days
|
||||||
|
FROM class_enrollments ce
|
||||||
|
JOIN users u ON ce.user_id = u.id
|
||||||
|
WHERE ce.classroom_id = ?
|
||||||
|
""", (classroom_id,)).fetchall()
|
||||||
|
|
||||||
|
matrix = []
|
||||||
|
for st in students:
|
||||||
|
solved = conn.execute("SELECT chapter_id, is_solved FROM chapter_progress WHERE user_id = ?", (st["id"],)).fetchall()
|
||||||
|
matrix.append({
|
||||||
|
"student": dict(st),
|
||||||
|
"solved_chapters": {r["chapter_id"]: bool(r["is_solved"]) for r in solved}
|
||||||
|
})
|
||||||
|
conn.close()
|
||||||
|
return self.send_json({"matrix": matrix})
|
||||||
|
|
||||||
|
elif path == "/api/stats/leaderboard":
|
||||||
|
conn = get_db()
|
||||||
|
rows = conn.execute("SELECT name, xp, level, role FROM users WHERE role = 'student' ORDER BY xp DESC LIMIT 20").fetchall()
|
||||||
|
conn.close()
|
||||||
|
return self.send_json({"leaderboard": [dict(r) for r in rows]})
|
||||||
|
|
||||||
|
# --- STATISCHE DATEIEN ---
|
||||||
|
return super().do_GET()
|
||||||
|
|
||||||
|
def do_POST(self):
|
||||||
|
parsed = urllib.parse.urlparse(self.path)
|
||||||
|
path = parsed.path
|
||||||
|
|
||||||
|
try:
|
||||||
|
body = self.read_json_body()
|
||||||
|
except Exception:
|
||||||
|
return self.send_json({"error": "Ungültiges JSON"}, 400)
|
||||||
|
|
||||||
|
# 1. REGISTRIERUNG
|
||||||
|
if path == "/api/auth/register":
|
||||||
|
email = body.get("email", "").strip().lower()
|
||||||
|
password = body.get("password", "")
|
||||||
|
name = body.get("name", "").strip()
|
||||||
|
role = body.get("role", "student")
|
||||||
|
|
||||||
|
if not email or not password or not name:
|
||||||
|
return self.send_json({"error": "Name, E-Mail und Passwort sind erforderlich"}, 400)
|
||||||
|
if len(password) < 6:
|
||||||
|
return self.send_json({"error": "Passwort muss mindestens 6 Zeichen lang sein"}, 400)
|
||||||
|
|
||||||
|
pwd_hash, salt = hash_password(password)
|
||||||
|
conn = get_db()
|
||||||
|
try:
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute("INSERT INTO users (email, password_hash, salt, name, role) VALUES (?, ?, ?, ?, ?)",
|
||||||
|
(email, pwd_hash, salt, name, role))
|
||||||
|
user_id = c.lastrowid
|
||||||
|
conn.commit()
|
||||||
|
token = generate_token(user_id, email, role)
|
||||||
|
return self.send_json({
|
||||||
|
"message": "Erfolgreich registriert!",
|
||||||
|
"token": token,
|
||||||
|
"user": {"id": user_id, "email": email, "name": name, "role": role, "xp": 0, "level": 1}
|
||||||
|
}, 201)
|
||||||
|
except sqlite3.IntegrityError:
|
||||||
|
return self.send_json({"error": "Ein Benutzer mit dieser E-Mail existiert bereits"}, 409)
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# 2. LOGIN
|
||||||
|
elif path == "/api/auth/login":
|
||||||
|
email = body.get("email", "").strip().lower()
|
||||||
|
password = body.get("password", "")
|
||||||
|
|
||||||
|
conn = get_db()
|
||||||
|
user = conn.execute("SELECT * FROM users WHERE email = ?", (email,)).fetchone()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if not user or not verify_password(password, user["password_hash"], user["salt"]):
|
||||||
|
return self.send_json({"error": "Ungültige E-Mail-Adresse oder Passwort"}, 401)
|
||||||
|
|
||||||
|
token = generate_token(user["id"], user["email"], user["role"])
|
||||||
|
return self.send_json({
|
||||||
|
"message": "Erfolgreich angemeldet!",
|
||||||
|
"token": token,
|
||||||
|
"user": {
|
||||||
|
"id": user["id"],
|
||||||
|
"email": user["email"],
|
||||||
|
"name": user["name"],
|
||||||
|
"role": user["role"],
|
||||||
|
"xp": user["xp"],
|
||||||
|
"level": user["level"],
|
||||||
|
"streak_days": user["streak_days"]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# 3. CODE SPEICHERN (AUTO-SAVE)
|
||||||
|
elif path == "/api/progress/save":
|
||||||
|
user_token = self.get_auth_user()
|
||||||
|
if not user_token:
|
||||||
|
return self.send_json({"error": "Nicht authentifiziert"}, 401)
|
||||||
|
|
||||||
|
chapter_id = body.get("chapter_id", "")
|
||||||
|
code_draft = body.get("code_draft", "")
|
||||||
|
subgoals_json = json.dumps(body.get("subgoals", {}))
|
||||||
|
|
||||||
|
if not chapter_id:
|
||||||
|
return self.send_json({"error": "chapter_id erforderlich"}, 400)
|
||||||
|
|
||||||
|
conn = get_db()
|
||||||
|
conn.execute("""
|
||||||
|
INSERT INTO chapter_progress (user_id, chapter_id, code_draft, subgoals_json, updated_at)
|
||||||
|
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||||
|
ON CONFLICT(user_id, chapter_id) DO UPDATE SET
|
||||||
|
code_draft = excluded.code_draft,
|
||||||
|
subgoals_json = excluded.subgoals_json,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
""", (user_token["uid"], chapter_id, code_draft, subgoals_json))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return self.send_json({"status": "saved", "chapter_id": chapter_id})
|
||||||
|
|
||||||
|
# 4. KAPITEL ALS GELÖST MARKIEREN & XP VERGEBEN
|
||||||
|
elif path == "/api/progress/solve":
|
||||||
|
user_token = self.get_auth_user()
|
||||||
|
if not user_token:
|
||||||
|
return self.send_json({"error": "Nicht authentifiziert"}, 401)
|
||||||
|
|
||||||
|
chapter_id = body.get("chapter_id", "")
|
||||||
|
xp_reward = int(body.get("xp_reward", 100))
|
||||||
|
|
||||||
|
if not chapter_id:
|
||||||
|
return self.send_json({"error": "chapter_id erforderlich"}, 400)
|
||||||
|
|
||||||
|
conn = get_db()
|
||||||
|
# Prüfe, ob das Kapitel bereits gelöst war
|
||||||
|
existing = conn.execute("SELECT is_solved FROM chapter_progress WHERE user_id = ? AND chapter_id = ?", (user_token["uid"], chapter_id)).fetchone()
|
||||||
|
|
||||||
|
was_already_solved = existing and existing["is_solved"] == 1
|
||||||
|
|
||||||
|
if not was_already_solved:
|
||||||
|
conn.execute("""
|
||||||
|
INSERT INTO chapter_progress (user_id, chapter_id, is_solved, solved_at, updated_at)
|
||||||
|
VALUES (?, ?, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||||
|
ON CONFLICT(user_id, chapter_id) DO UPDATE SET
|
||||||
|
is_solved = 1,
|
||||||
|
solved_at = CURRENT_TIMESTAMP,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
""", (user_token["uid"], chapter_id))
|
||||||
|
|
||||||
|
# XP & Level berechnen
|
||||||
|
user_row = conn.execute("SELECT xp FROM users WHERE id = ?", (user_token["uid"],)).fetchone()
|
||||||
|
new_xp = (user_row["xp"] if user_row else 0) + xp_reward
|
||||||
|
new_level = (new_xp // 100) + 1
|
||||||
|
|
||||||
|
conn.execute("UPDATE users SET xp = ?, level = ? WHERE id = ?", (new_xp, new_level, user_token["uid"]))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return self.send_json({"status": "solved", "earned_xp": xp_reward, "total_xp": new_xp, "level": new_level})
|
||||||
|
else:
|
||||||
|
conn.close()
|
||||||
|
return self.send_json({"status": "already_solved", "earned_xp": 0})
|
||||||
|
|
||||||
|
# 5. KLASSENRAUM ERSTELLEN (LEHRER)
|
||||||
|
elif path == "/api/classrooms/create":
|
||||||
|
user_token = self.get_auth_user()
|
||||||
|
if not user_token or user_token.get("role") != "teacher":
|
||||||
|
return self.send_json({"error": "Nur für Lehrkräfte gestattet"}, 403)
|
||||||
|
|
||||||
|
name = body.get("name", "").strip()
|
||||||
|
if not name:
|
||||||
|
return self.send_json({"error": "Klassenname erforderlich"}, 400)
|
||||||
|
|
||||||
|
invite_code = "PY-" + secrets.token_hex(3).upper()
|
||||||
|
conn = get_db()
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute("INSERT INTO classrooms (teacher_id, name, invite_code) VALUES (?, ?, ?)",
|
||||||
|
(user_token["uid"], name, invite_code))
|
||||||
|
class_id = c.lastrowid
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return self.send_json({"classroom": {"id": class_id, "name": name, "invite_code": invite_code}}, 201)
|
||||||
|
|
||||||
|
# 6. KLASSENRAUM BEITRETEN (SCHÜLER)
|
||||||
|
elif path == "/api/classrooms/join":
|
||||||
|
user_token = self.get_auth_user()
|
||||||
|
if not user_token:
|
||||||
|
return self.send_json({"error": "Nicht authentifiziert"}, 401)
|
||||||
|
|
||||||
|
code = body.get("invite_code", "").strip().upper()
|
||||||
|
conn = get_db()
|
||||||
|
cl = conn.execute("SELECT id, name FROM classrooms WHERE invite_code = ?", (code,)).fetchone()
|
||||||
|
if not cl:
|
||||||
|
conn.close()
|
||||||
|
return self.send_json({"error": "Ungültiger Einladungscode"}, 404)
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn.execute("INSERT INTO class_enrollments (classroom_id, user_id) VALUES (?, ?)", (cl["id"], user_token["uid"]))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return self.send_json({"message": f"Erfolgreich Klasse '{cl['name']}' beigetreten!", "classroom": dict(cl)})
|
||||||
|
except sqlite3.IntegrityError:
|
||||||
|
conn.close()
|
||||||
|
return self.send_json({"message": "Du bist dieser Klasse bereits beigetreten.", "classroom": dict(cl)})
|
||||||
|
|
||||||
|
return self.send_json({"error": "Endpunkt nicht gefunden"}, 404)
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# SERVER START
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
def run_server():
|
||||||
|
init_db()
|
||||||
|
handler = PlatformRequestHandler
|
||||||
|
socketserver.TCPServer.allow_reuse_address = True
|
||||||
|
with socketserver.TCPServer(("", PORT), handler) as httpd:
|
||||||
|
print(f"🚀 Python Lernplattform Server läuft auf: http://localhost:{PORT}")
|
||||||
|
print(f"📁 Root-Verzeichnis: {BASE_DIR}")
|
||||||
|
print(f"💾 SQLite-Datenbank: {DB_FILE}")
|
||||||
|
try:
|
||||||
|
httpd.serve_forever()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n🛑 Server gestoppt.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run_server()
|
||||||
248
teacher.html
Normal file
248
teacher.html
Normal file
|
|
@ -0,0 +1,248 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Lehrer-Dashboard | Klassen & Lernfortschritt</title>
|
||||||
|
<link rel="stylesheet" href="assets/style.css">
|
||||||
|
<style>
|
||||||
|
.class-card {
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
.class-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.invite-code-box {
|
||||||
|
background: #e0e7ff;
|
||||||
|
color: #3730a3;
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
.student-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 14px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.student-table th, .student-table td {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.student-table th {
|
||||||
|
background: #f8fafc;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="app-layout">
|
||||||
|
|
||||||
|
<aside class="app-sidebar">
|
||||||
|
<div class="sidebar-header">
|
||||||
|
<a href="index.html" class="sidebar-brand-link">
|
||||||
|
<div class="sidebar-logo">🐍</div>
|
||||||
|
<div>
|
||||||
|
<div class="sidebar-title">Python Portal</div>
|
||||||
|
<div class="sidebar-subtitle">Lehrerbereich</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-quick-nav">
|
||||||
|
<a href="index.html" class="sidebar-link">🏠 Schüler-Dashboard</a>
|
||||||
|
<a href="teacher.html" class="sidebar-link active">👨🏫 Klassenverwaltung</a>
|
||||||
|
<a href="index.html#zertifikate" class="sidebar-link">🏆 Zertifikate</a>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div class="app-main">
|
||||||
|
<header>
|
||||||
|
<div class="header-container">
|
||||||
|
<div class="header-badge">👨🏫 Dozenten- & Lehrer-Portal</div>
|
||||||
|
<h1>Klassenverwaltung & Lernfortschritts-Matrix</h1>
|
||||||
|
<p>Behalte den Überblick über deine Klassen, lade Schüler per Code ein und verfolge gelöste Aufgaben in Echtzeit.</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="container">
|
||||||
|
|
||||||
|
<!-- Neue Klasse erstellen -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>✨ Neue Schulklasse / Kursgruppe anlegen</h2>
|
||||||
|
<form id="create-class-form" style="display: flex; gap: 12px; flex-wrap: wrap; margin-top: 14px;">
|
||||||
|
<input type="text" id="input-class-name" required placeholder="Klassenname (z.B. Informatik 10a 2026/27)" style="flex: 1; min-width: 260px; padding: 10px 14px; border: 1px solid var(--border-color); border-radius: var(--radius-sm); font-size: 0.95rem;">
|
||||||
|
<button type="submit" class="btn" style="padding: 10px 20px;">➕ Klasse erstellen</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Vorhandene Klassen -->
|
||||||
|
<section>
|
||||||
|
<div class="section-header">
|
||||||
|
<h2>📚 Deine aktiven Klassen</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="classrooms-list-container">
|
||||||
|
<div class="card" style="text-align: center; color: var(--text-muted); padding: 40px;">
|
||||||
|
Lade Klassenräume...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="assets/app_auth.js"></script>
|
||||||
|
<script>
|
||||||
|
async function loadClassrooms() {
|
||||||
|
const container = document.getElementById("classrooms-list-container");
|
||||||
|
const token = localStorage.getItem("auth_token");
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="card" style="text-align: center; padding: 30px;">
|
||||||
|
<p>Du musst als <strong>Lehrkraft</strong> angemeldet sein, um Klassen zu verwalten.</p>
|
||||||
|
<button class="btn" onclick="window.openAuthModal('login')">🔑 Als Lehrer anmelden</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/classrooms/list", {
|
||||||
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
const classrooms = data.classrooms || [];
|
||||||
|
|
||||||
|
if (classrooms.length === 0) {
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="card" style="text-align: center; padding: 30px; color: var(--text-muted);">
|
||||||
|
Noch keine Klassen angelegt. Erstelle oben deine erste Klasse!
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = classrooms.map(cl => `
|
||||||
|
<div class="class-card">
|
||||||
|
<div class="class-header">
|
||||||
|
<div>
|
||||||
|
<h3 style="margin: 0 0 4px 0; color: var(--text-main); font-size: 1.25rem;">${cl.name}</h3>
|
||||||
|
<span style="font-size: 0.85rem; color: var(--text-muted);">${cl.student_count || 0} Schüler eingeschrieben</span>
|
||||||
|
</div>
|
||||||
|
<div style="text-align: right;">
|
||||||
|
<div style="font-size: 0.72rem; color: var(--text-muted); margin-bottom: 2px;">EINLADUNGSCODE:</div>
|
||||||
|
<div class="invite-code-box">${cl.invite_code}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-secondary" style="padding: 6px 12px; font-size: 0.82rem; margin-top: 10px;" onclick="loadClassMatrix(${cl.id}, '${cl.name}')">📊 Lernmatrix & Fortschritt anzeigen</button>
|
||||||
|
<div id="matrix-container-${cl.id}" style="display: none; margin-top: 14px;"></div>
|
||||||
|
</div>
|
||||||
|
`).join("");
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
container.innerHTML = `<div class="box box-danger">Fehler beim Laden: ${err.message}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadClassMatrix(classId, className) {
|
||||||
|
const target = document.getElementById(`matrix-container-${classId}`);
|
||||||
|
if (target.style.display === "block") {
|
||||||
|
target.style.display = "none";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
target.style.display = "block";
|
||||||
|
target.innerHTML = "Lade Schüler-Fortschritt...";
|
||||||
|
|
||||||
|
const token = localStorage.getItem("auth_token");
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/classrooms/matrix?classroom_id=${classId}`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
const matrix = data.matrix || [];
|
||||||
|
|
||||||
|
if (matrix.length === 0) {
|
||||||
|
target.innerHTML = "<p style='color: var(--text-muted); font-size: 0.88rem;'>Noch keine Schüler in dieser Klasse beigetreten.</p>";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
target.innerHTML = `
|
||||||
|
<table class="student-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Schüler</th>
|
||||||
|
<th>Level</th>
|
||||||
|
<th>XP</th>
|
||||||
|
<th>Streak</th>
|
||||||
|
<th>Gelöste Kapitel</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${matrix.map(m => `
|
||||||
|
<tr>
|
||||||
|
<td><strong>${m.student.name}</strong><br><span style="font-size: 0.75rem; color: #64748b;">${m.student.email}</span></td>
|
||||||
|
<td>⭐ Level ${m.student.level}</td>
|
||||||
|
<td><span style="font-weight: 700; color: #0284c7;">${m.student.xp} XP</span></td>
|
||||||
|
<td>🔥 ${m.student.streak_days} Tage</td>
|
||||||
|
<td><span style="background: #ecfdf5; color: #059669; padding: 2px 8px; border-radius: 99px; font-weight: 700; font-size: 0.8rem;">${Object.values(m.solved_chapters).filter(Boolean).length} / 27</span></td>
|
||||||
|
</tr>
|
||||||
|
`).join("")}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
`;
|
||||||
|
} catch (e) {
|
||||||
|
target.innerHTML = `<span style="color: var(--danger);">Fehler: ${e.message}</span>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("create-class-form").onsubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const name = document.getElementById("input-class-name").value.trim();
|
||||||
|
const token = localStorage.getItem("auth_token");
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
window.openAuthModal("login");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/classrooms/create", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
||||||
|
body: JSON.stringify({ name })
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok) throw new Error(data.error || "Fehler beim Erstellen");
|
||||||
|
|
||||||
|
alert(`Klasse '${name}' mit Einladungscode '${data.classroom.invite_code}' erstellt! 🎉`);
|
||||||
|
document.getElementById("input-class-name").value = "";
|
||||||
|
loadClassrooms();
|
||||||
|
} catch (err) {
|
||||||
|
alert("Fehler: " + err.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", loadClassrooms);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
372
workspace.html
Normal file
372
workspace.html
Normal file
|
|
@ -0,0 +1,372 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Python Web-IDE | Interaktiver Arbeitsbereich</title>
|
||||||
|
<link rel="stylesheet" href="assets/style.css">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100vh;
|
||||||
|
background: #090d16;
|
||||||
|
color: #f8fafc;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Workspace Topbar */
|
||||||
|
.workspace-topbar {
|
||||||
|
height: 52px;
|
||||||
|
background: #0f172a;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 18px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-title {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #ffffff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Workspace Split Screen Layout */
|
||||||
|
.workspace-body {
|
||||||
|
flex: 1;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 42% 58%;
|
||||||
|
height: calc(100vh - 52px);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Left Pane: Theory & Tasks */
|
||||||
|
.pane-left {
|
||||||
|
background: #ffffff;
|
||||||
|
color: var(--text-main);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border-right: 2px solid #e2e8f0;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pane-tabs {
|
||||||
|
display: flex;
|
||||||
|
background: #f8fafc;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
padding: 0 10px;
|
||||||
|
gap: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pane-tab-btn {
|
||||||
|
padding: 10px 14px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pane-tab-btn:hover {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pane-tab-btn.active {
|
||||||
|
color: var(--primary);
|
||||||
|
border-bottom-color: var(--primary);
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pane-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Right Pane: Code Editor & Terminal */
|
||||||
|
.pane-right {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
background: #0b0f19;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
height: 65%;
|
||||||
|
min-height: 200px;
|
||||||
|
position: relative;
|
||||||
|
background: #0b0f19;
|
||||||
|
}
|
||||||
|
|
||||||
|
#monaco-editor-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-wrapper {
|
||||||
|
height: 35%;
|
||||||
|
min-height: 140px;
|
||||||
|
background: #060911;
|
||||||
|
border-top: 2px solid rgba(255, 255, 255, 0.08);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-header {
|
||||||
|
padding: 6px 14px;
|
||||||
|
background: #090d16;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
#workspace-terminal-output {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px 16px;
|
||||||
|
overflow-y: auto;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.88rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #f1f5f9;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.term-stdout { color: #38bdf8; }
|
||||||
|
.term-stderr, .term-error { color: #f87171; font-weight: bold; }
|
||||||
|
.term-success { color: #4ade80; font-weight: bold; }
|
||||||
|
.term-warning { color: #fbbf24; font-weight: bold; }
|
||||||
|
.term-info { color: #94a3b8; }
|
||||||
|
|
||||||
|
/* Success Modal */
|
||||||
|
.modal-celebration {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(15, 23, 42, 0.85);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
z-index: 9999;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.workspace-body {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: 50% 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- Topbar -->
|
||||||
|
<div class="workspace-topbar">
|
||||||
|
<div class="workspace-title">
|
||||||
|
<a href="index.html" style="color: #94a3b8; text-decoration: none; font-size: 0.88rem;">🏠 Dashboard</a>
|
||||||
|
<span style="color: #64748b;">/</span>
|
||||||
|
<span id="workspace-chapter-name">Lerneinheit wird geladen...</span>
|
||||||
|
<span id="editor-save-indicator" style="font-size: 0.75rem; color: #10b981; opacity: 0.5; margin-left: 10px;">💾 Gespeichert</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="workspace-actions">
|
||||||
|
<button type="button" class="btn" id="btn-run-code" style="padding: 6px 14px; font-size: 0.84rem; background: #0284c7;">▶ Code ausführen</button>
|
||||||
|
<button type="button" class="btn" id="btn-run-tests" style="padding: 6px 16px; font-size: 0.84rem; background: #10b981;">🧪 Tests starten</button>
|
||||||
|
<button type="button" class="btn btn-secondary" id="btn-show-solution" style="padding: 6px 12px; font-size: 0.84rem; background: #1e293b; color: white; border: none;">💡 Lösung</button>
|
||||||
|
<button type="button" class="btn btn-secondary" id="btn-reset-code" style="padding: 6px 10px; font-size: 0.84rem; background: transparent; color: #94a3b8; border: none;" title="Auf Startercode zurücksetzen">🔄</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Split Screen Body -->
|
||||||
|
<div class="workspace-body">
|
||||||
|
|
||||||
|
<!-- Left Pane -->
|
||||||
|
<div class="pane-left">
|
||||||
|
<div class="pane-tabs">
|
||||||
|
<button type="button" class="pane-tab-btn active" onclick="switchTab('theorie')">📖 Theorie & Analogie</button>
|
||||||
|
<button type="button" class="pane-tab-btn" onclick="switchTab('subgoals')">🎯 Teilziele</button>
|
||||||
|
<button type="button" class="pane-tab-btn" id="tab-btn-puzzle" onclick="switchTab('puzzle')">🧩 Puzzle (+25 XP)</button>
|
||||||
|
<button type="button" class="pane-tab-btn" id="tab-btn-interpreter" onclick="switchTab('interpreter')">🩺 Dolmetscher</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pane-content" id="tab-content-theorie">
|
||||||
|
<div id="theory-loader" style="color: var(--text-muted);">Lade didaktische Erklärung...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pane-content" id="tab-content-subgoals" style="display: none;">
|
||||||
|
<h3 style="margin-top: 0; font-size: 1.15rem; color: var(--text-main);">🎯 Nummerierte Teilziele (Subgoals)</h3>
|
||||||
|
<p style="font-size: 0.9rem; color: var(--text-muted);">Folge diesen strukturierten Schritten in <code>aufgabe.py</code>:</p>
|
||||||
|
<div id="subgoals-list-container"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pane-content" id="tab-content-puzzle" style="display: none;">
|
||||||
|
<div id="puzzle-container"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pane-content" id="tab-content-interpreter" style="display: none;">
|
||||||
|
<h3 style="margin-top: 0; font-size: 1.15rem; color: var(--text-main);">🩺 Python Fehler-Dolmetscher</h3>
|
||||||
|
<p style="font-size: 0.88rem; color: var(--text-muted);">Füge eine Traceback-Fehlermeldung ein:</p>
|
||||||
|
<textarea id="workspace-error-input" style="width: 100%; height: 90px; border: 1px solid var(--border-color); border-radius: var(--radius-sm); padding: 8px; font-family: var(--font-mono); font-size: 0.88rem;"></textarea>
|
||||||
|
<button type="button" class="btn" style="margin-top: 8px;" onclick="interpretWorkspaceError()">🔍 Übersetzen & Erklären</button>
|
||||||
|
<div id="workspace-error-result" style="display: none; margin-top: 14px; padding: 12px; background: #f0fdf4; border-radius: var(--radius-sm); border: 1px solid #bbf7d0;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Pane -->
|
||||||
|
<div class="pane-right">
|
||||||
|
<div class="editor-wrapper">
|
||||||
|
<div id="monaco-editor-container"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="terminal-wrapper">
|
||||||
|
<div class="terminal-header">
|
||||||
|
<span>🖥️ Terminal / Test-Ausgabe</span>
|
||||||
|
<button type="button" id="btn-clear-terminal" style="background: transparent; border: none; color: #94a3b8; font-size: 0.75rem; cursor: pointer;">🧹 Leeren</button>
|
||||||
|
</div>
|
||||||
|
<div id="workspace-terminal-output">Klicke oben auf '▶ Code ausführen' oder '🧪 Tests starten'...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Success Modal -->
|
||||||
|
<div class="modal-celebration" id="success-modal-banner">
|
||||||
|
<div class="certificate-container" style="max-width: 480px; text-align: center; padding: 32px;">
|
||||||
|
<div style="font-size: 3rem; margin-bottom: 10px;">🎉 🏆 🌟</div>
|
||||||
|
<h2 style="margin: 0 0 10px 0; color: var(--text-main); font-size: 1.6rem;">Kapitel gemeistert!</h2>
|
||||||
|
<p style="color: var(--text-muted); font-size: 1rem; margin-bottom: 24px;">Alle Tests wurden erfolgreich bestanden. Du hast <strong>+100 XP</strong> verdient!</p>
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 10px; justify-content: center;">
|
||||||
|
<a href="index.html" class="btn btn-secondary" style="padding: 10px 18px;">Zurück zum Dashboard</a>
|
||||||
|
<button type="button" class="btn" id="btn-next-chapter" style="padding: 10px 22px;">Nächstes Kapitel →</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Scripts -->
|
||||||
|
<script src="assets/gamification_data.js"></script>
|
||||||
|
<script src="assets/gamification.js"></script>
|
||||||
|
<script src="assets/interpreter.js"></script>
|
||||||
|
<script src="assets/certificate.js"></script>
|
||||||
|
<script src="assets/parsons.js"></script>
|
||||||
|
<script src="assets/monaco_ide.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Tab Switcher
|
||||||
|
function switchTab(tabId) {
|
||||||
|
document.querySelectorAll(".pane-tab-btn").forEach(b => b.classList.remove("active"));
|
||||||
|
document.querySelectorAll(".pane-content").forEach(c => c.style.display = "none");
|
||||||
|
|
||||||
|
const targetBtn = event.target;
|
||||||
|
const targetContent = document.getElementById("tab-content-" + tabId);
|
||||||
|
if (targetBtn) targetBtn.classList.add("active");
|
||||||
|
if (targetContent) targetContent.style.display = "block";
|
||||||
|
}
|
||||||
|
|
||||||
|
// URL Parameters
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const trackFolder = urlParams.get("track") || "lehrpfad_1_grundlagen";
|
||||||
|
const chapterFolder = urlParams.get("chapter") || "01_erste_schritte_taschenrechner";
|
||||||
|
const chapterId = `${trackFolder}/${chapterFolder}`;
|
||||||
|
|
||||||
|
// Load Chapter Files
|
||||||
|
async function loadWorkspaceData() {
|
||||||
|
try {
|
||||||
|
const [aufgabeCode, testCode, loesungCode, htmlDoc] = await Promise.all([
|
||||||
|
fetch(`${trackFolder}/${chapterFolder}/aufgabe.py`).then(r => r.text()),
|
||||||
|
fetch(`${trackFolder}/${chapterFolder}/test_aufgabe.py`).then(r => r.text()),
|
||||||
|
fetch(`${trackFolder}/${chapterFolder}/musterloesung.py`).then(r => r.text()).catch(() => ""),
|
||||||
|
fetch(`${trackFolder}/${chapterFolder}/index.html`).then(r => r.text()).catch(() => "")
|
||||||
|
]);
|
||||||
|
|
||||||
|
document.getElementById("workspace-chapter-name").innerText = chapterFolder.replace(/_/g, " ").toUpperCase();
|
||||||
|
|
||||||
|
// Extract theory content from chapter index.html
|
||||||
|
if (htmlDoc) {
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(htmlDoc, "text/html");
|
||||||
|
const mainContent = doc.querySelector("main.container") || doc.body;
|
||||||
|
document.getElementById("tab-content-theorie").innerHTML = mainContent.innerHTML;
|
||||||
|
|
||||||
|
// Extract Subgoals
|
||||||
|
const subgoals = aufgabeCode.match(/# 🎯 TEILZIEL.*$/gm) || [];
|
||||||
|
const subContainer = document.getElementById("subgoals-list-container");
|
||||||
|
if (subgoals.length > 0) {
|
||||||
|
subContainer.innerHTML = subgoals.map((s, idx) => `
|
||||||
|
<div class="task-item" style="margin-bottom: 8px;">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="sub_check_${idx}">
|
||||||
|
<label class="task-label" for="sub_check_${idx}"><strong>${s.replace('#', '').trim()}</strong></label>
|
||||||
|
</div>
|
||||||
|
`).join("");
|
||||||
|
} else {
|
||||||
|
subContainer.innerHTML = "<p style='color: var(--text-muted);'>Keine nummerierten Teilziele vorhanden.</p>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Musterlösung Button
|
||||||
|
document.getElementById("btn-show-solution")?.addEventListener("click", () => {
|
||||||
|
if (confirm("Möchtest du einen Blick in die Musterlösung werfen? Versuche zuerst, es selbst zu lösen!")) {
|
||||||
|
const w = window.open("", "_blank");
|
||||||
|
w.document.write(`<pre style="background:#0b0f19;color:#f8fafc;padding:20px;font-family:monospace;font-size:14px;">${loesungCode}</pre>`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize Monaco IDE
|
||||||
|
window.initWorkspace({
|
||||||
|
chapterId: chapterId,
|
||||||
|
starterCode: aufgabeCode,
|
||||||
|
testCode: testCode,
|
||||||
|
solutionCode: loesungCode
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
document.getElementById("tab-content-theorie").innerHTML = `<div class="box box-danger">❌ Fehler beim Laden der Einheit: ${err.message}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function interpretWorkspaceError() {
|
||||||
|
const errText = document.getElementById("workspace-error-input").value;
|
||||||
|
const resContainer = document.getElementById("workspace-error-result");
|
||||||
|
if (window.INTERPRETER && window.INTERPRETER.translate) {
|
||||||
|
const info = window.INTERPRETER.translate(errText);
|
||||||
|
resContainer.style.display = "block";
|
||||||
|
resContainer.innerHTML = `
|
||||||
|
<h4 style="margin-top: 0; color: #166534;">${info.name}</h4>
|
||||||
|
<p style="margin: 6px 0; color: #1e293b;">${info.explanation}</p>
|
||||||
|
<strong>Tipps:</strong>
|
||||||
|
<ul style="padding-left: 20px; margin-top: 4px;">
|
||||||
|
${info.fixes.map(f => `<li>${f}</li>`).join("")}
|
||||||
|
</ul>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", loadWorkspaceData);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in a new issue