Abschlussprüfungstrainer – Dein digitales Prüfungszentrum
Mit dieser hochwertigen Webseite erhältst du ein vollwertiges Trainingssystem für Abschlussprüfungen. Das Design ist modern, professionell und speziell auf Schüler und Bildungseinrichtungen zugeschnitten.
Funktionen im Überblick:
- PDF-Upload alter Prüfungen – Lade bestehende Prüfungen hoch, das Skript extrahiert automatisch die Fragen.
- Test-Simulation – Schüler können wie in einer echten Prüfung trainieren: klare Darstellung, Fragen nacheinander, Antwortmöglichkeiten und strukturierter Ablauf.
- Zeitlimit mit Countdown – Jede Simulation läuft unter realistischen Bedingungen, inklusive Timer und Abgabe.
- User-Accounts für Schüler – Sichere Anmeldung mit persönlichem Dashboard, Übersicht der Tests und Ergebnisse.
- Admin-Bereich – Benutzerverwaltung, Upload von Prüfungen, Lösch- und Kontrollmöglichkeiten.
- Dashboard-Übersicht – Klare Statistiken, gespeicherte Ergebnisse, Auswertung der Prüfungssimulation.
- Automatische Ordner- und Dateistruktur – Alles wird aus einer Hauptdatei heraus erstellt, keine manuelle Konfiguration nötig.
- Professionelles Design – Optisch so hochwertig, als hätte es mehrere tausend Euro gekostet, responsive und intuitiv bedienbar.
Optionale Features:
- Textextraktion via
pdftotext
(falls auf dem Server verfügbar) - Anpassbare Prüfungsläufe (z. B. Fächer, Schwierigkeitsgrad)
- Fortschrittsanzeige und Ergebnisbewertung
Installation:
- Datei auf den Server laden
- MySQL-Zugangsdaten in der Datei eintragen
- Erste Anmeldung über Admin-Account (Standard: admin / admin123, Passwort bitte sofort ändern)
- Alte Prüfungen hochladen und erste Tests starten
Vorteile:
- Realistische Prüfungsvorbereitung rund um die Uhr
- Ideal für Schüler, Nachhilfelehrer oder Bildungseinrichtungen
- Keine teuren Zusatzprogramme notwendig, läuft direkt auf jedem gängigen Webserver
- Übersichtliche Auswertung sorgt für schnelle Lernfortschritte
Hinweis:
Dieses Skript ersetzt keine offiziellen Prüfungen, bietet aber eine optimale Vorbereitung unter nahezu realistischen Bedingungen.
<?php
// Konfiguration
$dbHost = '127.0.0.1';
$dbName = 'exam_trainer_db';
$dbUser = 'root';
$dbPass = ''; // setze hier das Passwort
$baseDir = __DIR__;
$uploadDir = __DIR__ . '/uploads';
$dataDir = __DIR__ . '/data';
if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true);
if (!is_dir($dataDir)) mkdir($dataDir, 0755, true);
// Mysql Verbindung und Tabellen anlegen falls noetig
try {
$pdo = new PDO("mysql:host={$dbHost};charset=utf8mb4", $dbUser, $dbPass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
// Datenbank erstellen falls nicht vorhanden
$pdo->exec("CREATE DATABASE IF NOT EXISTS `{$dbName}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
$pdo->exec("USE `{$dbName}`");
// Tabellen
$pdo->exec("CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(100) UNIQUE,
password VARCHAR(255),
display_name VARCHAR(200),
is_admin TINYINT(1) DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
$pdo->exec("CREATE TABLE IF NOT EXISTS exams (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255),
filename VARCHAR(255),
uploader INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (uploader) REFERENCES users(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
$pdo->exec("CREATE TABLE IF NOT EXISTS questions (
id INT AUTO_INCREMENT PRIMARY KEY,
exam_id INT,
qtext TEXT,
qnum VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (exam_id) REFERENCES exams(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
$pdo->exec("CREATE TABLE IF NOT EXISTS attempts (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT,
exam_id INT,
started_at TIMESTAMP NULL,
finished_at TIMESTAMP NULL,
score INT DEFAULT 0,
duration_sec INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (exam_id) REFERENCES exams(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
$pdo->exec("CREATE TABLE IF NOT EXISTS answers (
id INT AUTO_INCREMENT PRIMARY KEY,
attempt_id INT,
question_id INT,
answer_text TEXT,
correct TINYINT(1) DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (attempt_id) REFERENCES attempts(id) ON DELETE CASCADE,
FOREIGN KEY (question_id) REFERENCES questions(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
} catch (Exception $e) {
echo 'Datenbankfehler: ' . htmlspecialchars($e->getMessage());
exit;
}
session_start();
function json($d) { header('Content-Type: application/json; charset=utf-8'); echo json_encode($d); exit; }
// Hilfsfunktionen
function currentUserId() { return $_SESSION['user_id'] ?? null; }
function isAdmin() { return !empty($_SESSION['is_admin']); }
// initial admin anlegen falls noch keiner existiert
$stmt = $pdo->query("SELECT COUNT(*) as c FROM users"); $cnt = $stmt->fetch(PDO::FETCH_ASSOC)['c'];
if ($cnt == 0) {
$pw = password_hash('admin123', PASSWORD_DEFAULT);
$pdo->prepare("INSERT INTO users (username,password,display_name,is_admin) VALUES (?,?,?,1)")->execute(['admin',$pw,'Administrator']);
}
// API Endpunkte
$action = $_REQUEST['action'] ?? '';
if ($action === 'register' && $_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
$display = trim($_POST['display'] ?? $username);
if (!$username || !$password) json(['error' => 'Bitte Benutzername und Passwort angeben']);
try {
$pw = password_hash($password, PASSWORD_DEFAULT);
$stm = $pdo->prepare("INSERT INTO users (username,password,display_name) VALUES (?,?,?)");
$stm->execute([$username,$pw,$display]);
json(['success' => true]);
} catch (Exception $e) { json(['error' => 'Benutzer existiert bereits']); }
}
if ($action === 'login' && $_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
$stm = $pdo->prepare("SELECT * FROM users WHERE username = ? LIMIT 1");
$stm->execute([$username]); $u = $stm->fetch(PDO::FETCH_ASSOC);
if ($u && password_verify($password, $u['password'])) {
$_SESSION['user_id'] = $u['id']; $_SESSION['display_name'] = $u['display_name']; $_SESSION['is_admin'] = $u['is_admin'];
json(['success' => true]);
} else json(['error' => 'Ungültige Zugangsdaten']);
}
if ($action === 'logout') { session_destroy(); json(['success' => true]); }
if ($action === 'upload' && $_SERVER['REQUEST_METHOD'] === 'POST') {
if (!currentUserId()) json(['error' => 'Bitte einloggen']);
if (!isset($_FILES['pdf'])) json(['error' => 'Datei fehlt']);
$f = $_FILES['pdf'];
if ($f['error'] !== UPLOAD_ERR_OK) json(['error' => 'Upload Fehler']);
$ext = strtolower(pathinfo($f['name'], PATHINFO_EXTENSION));
if ($ext !== 'pdf') json(['error' => 'Nur PDF erlaubt']);
$fname = time() . '-' . bin2hex(random_bytes(6)) . '.pdf';
$target = $uploadDir . '/' . $fname;
if (!move_uploaded_file($f['tmp_name'], $target)) json(['error' => 'Speicherfehler']);
// eintrag in exams
$title = trim($_POST['title'] ?? $f['name']);
$pdo->prepare("INSERT INTO exams (title,filename,uploader) VALUES (?,?,?)")->execute([$title,$fname,currentUserId()]);
$examId = $pdo->lastInsertId();
// versuch text zu extrahieren mit pdftotext falls vorhanden
$text = '';
$pdftotext = trim(shell_exec('which pdftotext'));
if ($pdftotext) {
$out = tempnam(sys_get_temp_dir(), 'txt');
@exec(escapeshellcmd("{$pdftotext} " . escapeshellarg($target) . " " . escapeshellarg($out) . " 2>&1"));
$text = @file_get_contents($out) ?: '';
@unlink($out);
} else {
// fallback leer
$text = '';
}
// einfache frage extraktion: split nach zeilen die mit Zahl beginnen oder mit Wort Frage
$questions = [];
if ($text) {
$lines = preg_split('/\r?\n/', $text);
$buffer = '';
$qnum = 0;
foreach ($lines as $ln) {
$ln = trim($ln);
if (!$ln) continue;
if (preg_match('/^(Frage|F|[0-9]{1,3}[\.|\)] )/i', $ln) || preg_match('/^[0-9]+\./', $ln) ) {
if ($buffer) { $questions[] = ['qnum' => ++$qnum, 'text' => $buffer]; }
$buffer = $ln;
} else {
$buffer .= ' ' . $ln;
}
}
if ($buffer) $questions[] = ['qnum' => ++$qnum, 'text' => $buffer];
}
// wenn keine fragen automatisch gefunden wurden, lege placeholder fragen an
if (count($questions) === 0) {
for ($i=1;$i<=10;$i++) $questions[] = ['qnum' => $i, 'text' => "Beispielfrage {$i} aus dem Upload bitte manuell prüfen"];
}
// speicher fragen
$ins = $pdo->prepare("INSERT INTO questions (exam_id,qtext,qnum) VALUES (?,?,?)");
foreach ($questions as $q) $ins->execute([$examId, $q['text'], $q['qnum']]);
json(['success' => true, 'exam_id' => $examId, 'questions' => count($questions)]);
}
if ($action === 'list_exams') {
$res = $pdo->query("SELECT e.*, u.display_name FROM exams e LEFT JOIN users u ON u.id = e.uploader ORDER BY e.created_at DESC");
$out = [];
while ($r = $res->fetch(PDO::FETCH_ASSOC)) $out[] = $r;
json($out);
}
if ($action === 'get_questions' && isset($_GET['exam_id'])) {
$exam_id = intval($_GET['exam_id']);
$res = $pdo->prepare("SELECT * FROM questions WHERE exam_id = ? ORDER BY id ASC"); $res->execute([$exam_id]);
$out = [];
while ($r = $res->fetch(PDO::FETCH_ASSOC)) $out[] = $r;
json($out);
}
if ($action === 'start_attempt' && $_SERVER['REQUEST_METHOD'] === 'POST') {
if (!currentUserId()) json(['error' => 'Bitte einloggen']);
$exam_id = intval($_POST['exam_id']);
$time_limit = intval($_POST['time_limit']) ?? 1800; // default 30 min
$pdo->prepare("INSERT INTO attempts (user_id,exam_id,started_at) VALUES (?,?,NOW())")->execute([currentUserId(), $exam_id]);
$attempt_id = $pdo->lastInsertId();
json(['success' => true, 'attempt_id' => $attempt_id]);
}
if ($action === 'submit_answer' && $_SERVER['REQUEST_METHOD'] === 'POST') {
$attempt_id = intval($_POST['attempt_id']);
$question_id = intval($_POST['question_id']);
$answer = trim($_POST['answer'] ?? '');
// einfache Speicherung ohne automatische Bewertung
$stm = $pdo->prepare("INSERT INTO answers (attempt_id,question_id,answer_text) VALUES (?,?,?)");
$stm->execute([$attempt_id, $question_id, $answer]);
json(['success' => true]);
}
if ($action === 'finish_attempt' && $_SERVER['REQUEST_METHOD'] === 'POST') {
$attempt_id = intval($_POST['attempt_id']);
// berechne dauer als beispiel
$stm = $pdo->prepare("SELECT started_at FROM attempts WHERE id = ?"); $stm->execute([$attempt_id]); $row = $stm->fetch(PDO::FETCH_ASSOC);
$started = strtotime($row['started_at']); $dur = time() - $started;
$pdo->prepare("UPDATE attempts SET finished_at = NOW(), duration_sec = ? WHERE id = ?")->execute([$dur, $attempt_id]);
json(['success' => true, 'duration' => $dur]);
}
if ($action === 'admin_delete_exam' && $_SERVER['REQUEST_METHOD'] === 'POST') {
if (!isAdmin()) json(['error' => 'Zugriff verweigert']);
$exam_id = intval($_POST['exam_id']);
// entferne datei
$stm = $pdo->prepare("SELECT filename FROM exams WHERE id = ?"); $stm->execute([$exam_id]); $r = $stm->fetch(PDO::FETCH_ASSOC);
if ($r && $r['filename']) @unlink($uploadDir . '/' . $r['filename']);
$pdo->prepare("DELETE FROM exams WHERE id = ?")->execute([$exam_id]);
json(['success' => true]);
}
// Ende API logik
// UI
?><!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Dreamcodes - Abschlussprüfungstrainer</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap" rel="stylesheet">
<style>
:root{--bg:#071027;--card:#0b1220;--accent:#ff7a59;--muted:#9aa4b2}
body{margin:0;font-family:Inter,system-ui,Arial;background:linear-gradient(180deg,var(--bg),#021022);color:#e6eef8;padding:36px}
.container{max-width:1100px;margin:0 auto}
.header{display:flex;justify-content:space-between;align-items:center}
.brand{font-weight:700;font-size:20px}
.card{background:linear-gradient(180deg,var(--card),#071022);padding:18px;border-radius:12px;box-shadow:0 12px 40px rgba(2,6,23,0.7);margin-top:18px}
.grid{display:grid;grid-template-columns:1fr 360px;gap:18px}
.input,select{background:transparent;border:1px solid rgba(255,255,255,0.04);padding:10px;border-radius:8px;color:inherit;width:100%}
.btn{background:var(--accent);border:none;padding:10px 14px;border-radius:10px;color:#081018;font-weight:700;cursor:pointer}
.list{max-height:480px;overflow:auto}
.exam{padding:10px;border-radius:8px;background:rgba(255,255,255,0.02);margin-bottom:10px}
.small{font-size:13px;color:var(--muted)}
.badge{background:rgba(255,255,255,0.04);padding:6px 10px;border-radius:999px;font-weight:600}
.timer{font-size:20px;font-weight:700}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="brand">Abschlussprüfungstrainer</div>
<div id="userbox" class="small"></div>
</div>
<div class="card grid">
<div>
<div class="card">
<h3>Prüfung Upload</h3>
<form id="uploadForm" enctype="multipart/form-data">
<input type="text" name="title" placeholder="Titel der Prüfung" class="input" />
<input type="file" name="pdf" accept="application/pdf" class="input" />
<div style="margin-top:10px;display:flex;gap:8px;align-items:center">
<button class="btn" type="submit">Hochladen und Fragen extrahieren</button>
<div class="small">pdftotext wird verwendet falls installiert</div>
</div>
</form>
</div>
<div class="card">
<h3>Verfügbare Prüfungen</h3>
<div id="exams" class="list"></div>
</div>
</div>
<div>
<div class="card">
<h4>Benutzer</h4>
<div id="authbox"></div>
</div>
<div class="card">
<h4>Dashboard</h4>
<div id="dashboard"></div>
</div>
</div>
</div>
</div>
<script>
async function api(action, data){
const opts = { method: 'POST' };
if (data instanceof FormData) opts.body = data; else opts.body = new URLSearchParams(data || {});
const res = await fetch('?action=' + action, opts);
return res.json();
}
async function loadExams(){
const res = await fetch('?action=list_exams');
const exams = await res.json();
const el = document.getElementById('exams'); el.innerHTML = '';
exams.forEach(e => {
const d = document.createElement('div'); d.className = 'exam';
d.innerHTML = `<div style="display:flex;justify-content:space-between;align-items:center"><div><strong>${e.title}</strong><div class="small">von ${e.display_name} · ${e.created_at}</div></div><div><button class='btn' onclick='startExam(${e.id})'>Prüfung starten</button> <button style='background:#ef4444;color:#fff' onclick='adminDelete(${e.id})'>Löschen</button></div></div>`;
el.appendChild(d);
});
}
async function startExam(id){
if(!confirm('Starte Prüfung mit Zeitlimit 30 Minuten?')) return;
const res = await api('start_attempt', { exam_id: id, time_limit: 1800 });
if(res.error) alert(res.error); else location.href = '?exam=' + res.attempt_id;
}
async function adminDelete(id){ if(!confirm('Prüfung wirklich löschen')) return; const fd = new URLSearchParams(); fd.append('exam_id', id); const res = await api('admin_delete_exam', fd); if(res.success) loadExams(); else alert(res.error);
}
// upload
document.getElementById('uploadForm').addEventListener('submit', async function(e){
e.preventDefault();
const fd = new FormData(this);
const res = await api('upload', fd);
if(res.error) alert(res.error); else { alert('Upload OK. Gefundene Fragen ' + res.questions); loadExams(); }
});
// simple auth UI
async function loadAuth(){
const uid = await fetch('?action=whoami').then(r=>r.json()).catch(()=>null);
}
// check for attempt view
(function(){
const params = new URLSearchParams(location.search);
if(params.has('exam')){
const attemptId = params.get('exam');
// render exam interface in full page
document.body.innerHTML = `<div style="padding:30px;color:#fff;font-family:Inter"><div style='max-width:900px;margin:0 auto'><h2>Prüfung</h2><div id='examroot'></div></div></div>`;
renderExam(attemptId);
} else {
loadExams();
}
})();
async function renderExam(attemptId){
const root = document.getElementById('examroot');
// fetch questions by exam id mapping
// for demo wir nehmen exam id aus attempt
const att = await fetch('?action=get_attempt&attempt_id=' + attemptId).then(r=>r.json());
if(att.error){ root.innerHTML = '<div>Error</div>'; return; }
const examId = att.exam_id;
const qs = await fetch('?action=get_questions&exam_id=' + examId).then(r=>r.json());
root.innerHTML = `<div class='card' style='padding:20px'><div style='display:flex;justify-content:space-between;align-items:center'><div><strong>${att.title}</strong><div class='small'>Zeitlimit 30 Minuten</div></div><div class='timer' id='timer'>30:00</div></div><hr><div id='qwrap'></div><div style='margin-top:12px'><button id='finishBtn' class='btn'>Prüfung beenden</button></div></div>`;
const qwrap = document.getElementById('qwrap');
qs.forEach((q,i)=>{
const div = document.createElement('div'); div.className='exam'; div.innerHTML = `<div><strong>Frage ${i+1}</strong><p>${q.qtext}</p><textarea id='ans_${q.id}' style='width:100%;min-height:80px' placeholder='Antwort hier'></textarea></div>`;
qwrap.appendChild(div);
});
// countdown simple
let sec = 30 * 60; const timerEl = document.getElementById('timer');
const interval = setInterval(()=>{ sec--; const m = Math.floor(sec/60); const s = sec%60; timerEl.textContent = (m<10? '0'+m:m) + ':' + (s<10? '0'+s:s); if(sec<=0){ clearInterval(interval); submitAll(); } },1000);
async function submitAll(){
for(const q of qs){ const ans = document.getElementById('ans_'+q.id).value; const fd = new URLSearchParams(); fd.append('attempt_id', attemptId); fd.append('question_id', q.id); fd.append('answer', ans); await api('submit_answer', fd); }
const fd2 = new URLSearchParams(); fd2.append('attempt_id', attemptId); const res = await api('finish_attempt', fd2); alert('Prüfung beendet Dauer Sek ' + res.duration); location.href = './'; }
document.getElementById('finishBtn').addEventListener('click', async ()=>{ if(confirm('Prüfung wirklich beenden')){ submitAll(); }});
}
</script>
<div class="footer" style="text-align:center;color:#9aa4b2;margin-top:20px;font-size:13px;">
Powered by <a href="http://www.dreamcodes.net" target="_blank" style="color:#06b6d4;">Dreamcodes</a>
</div>
</body>
</html>