Initial commit: Modernes Harry Potter Escape Quiz Portal (FastAPI & MariaDB)

This commit is contained in:
kobold-ai
2026-09-21 11:29:18 +02:00
commit c3a21369ef
50 changed files with 3346 additions and 0 deletions
+236
View File
@@ -0,0 +1,236 @@
{% extends "base.html" %}
{% block title %}⚡ Zaubereiministerium – Cockpit{% endblock %}
{% block content %}
<div class="admin-wrapper">
<div class="admin-header">
<div>
<h1 class="glow-title">⚡ Großinquisitor-Cockpit</h1>
<p class="subtitle">Eingeloggt als: <strong>{{ admin_user }}</strong></p>
</div>
<div class="admin-top-actions">
<a href="/" target="_blank" class="btn btn-secondary">🌐 Zum Live-Quiz</a>
<a href="/admin/logout" class="btn btn-danger">🚪 Abmelden</a>
</div>
</div>
<!-- Navigation Tabs -->
<div class="tabs-nav">
<button class="tab-btn active" onclick="showTab('tab-pending')">
📥 Eingereichte Vorschläge
{% if pending_questions|length > 0 %}
<span class="badge badge-warning">{{ pending_questions|length }}</span>
{% endif %}
</button>
<button class="tab-btn" onclick="showTab('tab-active')">
📜 Aktive Quiz-Fragen
<span class="badge badge-info">{{ active_questions|length }}</span>
</button>
<button class="tab-btn" onclick="showTab('tab-new')">
➕ Neue Frage anlegen
</button>
{% if rejected_questions|length > 0 %}
<button class="tab-btn" onclick="showTab('tab-rejected')">
❌ Archiviert / Abgelehnt
<span class="badge badge-muted">{{ rejected_questions|length }}</span>
</button>
{% endif %}
</div>
<!-- TAB 1: Eingereichte Vorschläge (User Questions Need Test) -->
<section id="tab-pending" class="tab-content active">
<h2>📥 Eingegangene Vorschläge (Prüfung & Validierung erforderlich)</h2>
<p class="description">Diese Fragen wurden von Besuchern eingereicht. Bitte teste die Antwort und schalte die Frage frei oder lehne sie ab.</p>
{% if pending_questions|length == 0 %}
<div class="empty-state">
<p>🎉 Keine offenen Vorschläge zur Prüfung vorhanden!</p>
</div>
{% else %}
<div class="questions-list">
{% for q in pending_questions %}
<div class="card question-admin-card" id="q-card-{{ q.id }}">
<div class="q-header">
<span class="badge badge-warning">⚠️ Status: Prüfung ausstehend</span>
<span class="q-submitter">Von: <strong>{{ q.submitted_by or 'Gast' }}</strong> am {{ q.created_at.strftime('%d.%m.%Y %H:%M') }}</span>
</div>
<div class="q-body">
<h3>{{ q.question }}</h3>
<p><strong>Erwartete Antwort:</strong> <span class="highlight-answer">{{ q.canonical_answer }}</span></p>
{% if q.hint %}
<p><strong>Tipp:</strong> <em>{{ q.hint }}</em></p>
{% endif %}
<!-- Interaktives Testfeld für den Admin -->
<div class="test-matcher-box">
<label>🧪 Antwort-Validierung testen:</label>
<div class="test-input-group">
<input type="text" id="test-input-{{ q.id }}" class="input-text-sm" placeholder="Test-Eingabe (z.B. mit Tippfehler)">
<button type="button" class="btn btn-sm btn-secondary" onclick="testMatcher({{ q.id }}, '{{ q.canonical_answer|escape }}')">Testen</button>
</div>
<div id="test-result-{{ q.id }}" class="test-result-feedback"></div>
</div>
</div>
<div class="q-actions">
<button class="btn btn-success" onclick="approveQuestion({{ q.id }})">
✅ Freischalten & Aktivieren
</button>
<button class="btn btn-secondary" onclick="toggleEdit({{ q.id }})">
✏️ Bearbeiten
</button>
<button class="btn btn-danger" onclick="rejectQuestion({{ q.id }})">
❌ Ablehnen
</button>
</div>
<!-- Inline Edit Form (Hidden by default) -->
<div id="edit-form-{{ q.id }}" class="inline-edit-form hidden">
<form action="/admin/questions/{{ q.id }}/edit" method="post">
<div class="form-group">
<label>Frage:</label>
<input type="text" name="question" class="input-text" value="{{ q.question }}" required>
</div>
<div class="form-group">
<label>Richtige Antwort:</label>
<input type="text" name="canonical_answer" class="input-text" value="{{ q.canonical_answer }}" required>
</div>
<div class="form-group">
<label>Tipp:</label>
<input type="text" name="hint" class="input-text" value="{{ q.hint or '' }}">
</div>
<input type="hidden" name="status_val" value="active">
<button type="submit" class="btn btn-primary">Speichern & Aktivieren</button>
</form>
</div>
</div>
{% endfor %}
</div>
{% endif %}
</section>
<!-- TAB 2: Aktive Fragen -->
<section id="tab-active" class="tab-content">
<h2>📜 Aktive Fragen im Live-Quiz ({{ active_questions|length }})</h2>
<p class="description">Diese Fragen werden den Spielern in der angegebenen Reihenfolge gestellt.</p>
<div class="questions-list">
{% for q in active_questions %}
<div class="card question-admin-card" id="q-card-{{ q.id }}">
<div class="q-header">
<span class="badge badge-success">Reihenfolge #{{ q.sort_order }}</span>
<span class="q-category">Kategorie: {{ q.category }}</span>
</div>
<div class="q-body">
<h3>{{ loop.index }}. {{ q.question }}</h3>
<p><strong>Antwort:</strong> <span class="highlight-answer">{{ q.canonical_answer }}</span></p>
{% if q.hint %}
<p><strong>Tipp:</strong> <em>{{ q.hint }}</em></p>
{% endif %}
</div>
<div class="q-actions">
<button class="btn btn-secondary btn-sm" onclick="toggleEdit({{ q.id }})">✏️ Bearbeiten</button>
<button class="btn btn-danger btn-sm" onclick="deleteQuestion({{ q.id }})">🗑️ Löschen</button>
</div>
<!-- Inline Edit Form -->
<div id="edit-form-{{ q.id }}" class="inline-edit-form hidden">
<form action="/admin/questions/{{ q.id }}/edit" method="post">
<div class="form-group">
<label>Frage:</label>
<input type="text" name="question" class="input-text" value="{{ q.question }}" required>
</div>
<div class="form-group">
<label>Richtige Antwort:</label>
<input type="text" name="canonical_answer" class="input-text" value="{{ q.canonical_answer }}" required>
</div>
<div class="form-group">
<label>Tipp:</label>
<input type="text" name="hint" class="input-text" value="{{ q.hint or '' }}">
</div>
<div class="form-group">
<label>Reihenfolge (Sort Order):</label>
<input type="number" name="sort_order" class="input-text-sm" value="{{ q.sort_order }}">
</div>
<input type="hidden" name="status_val" value="active">
<button type="submit" class="btn btn-primary">Änderungen speichern</button>
</form>
</div>
</div>
{% endfor %}
</div>
</section>
<!-- TAB 3: Neue Frage anlegen -->
<section id="tab-new" class="tab-content">
<h2>➕ Neue Frage direkt zum Live-Quiz hinzufügen</h2>
<div class="card">
<form action="/admin/questions/new" method="post" class="magic-form">
<div class="form-group">
<label for="category">Kategorie:</label>
<select name="category" id="category" class="input-text">
<option value="harry_potter" selected>Harry Potter / Escape</option>
<option value="insider">Persönlich / Insider</option>
<option value="general">Allgemeines Rätsel</option>
</select>
</div>
<div class="form-group">
<label for="new_question">Frage:</label>
<textarea name="question" id="new_question" class="textarea" rows="2" required placeholder="z. B. Welcher Zauber entwaffnet Gegner?"></textarea>
</div>
<div class="form-group">
<label for="new_answer">Richtige Antwort:</label>
<input type="text" name="canonical_answer" id="new_answer" class="input-text" required placeholder="z. B. Expelliarmus">
</div>
<div class="form-group">
<label for="new_hint">Tipp (optional):</label>
<input type="text" name="hint" id="new_hint" class="input-text" placeholder="z. B. Harrys Lieblingszauber">
</div>
<div class="form-group">
<label for="new_sort">Reihenfolge (Nummerierung):</label>
<input type="number" name="sort_order" id="new_sort" class="input-text-sm" value="{{ active_questions|length + 1 }}">
</div>
<button type="submit" class="btn btn-primary btn-large">Frage speichern & aktivieren</button>
</form>
</div>
</section>
<!-- TAB 4: Archiviert / Abgelehnt -->
{% if rejected_questions|length > 0 %}
<section id="tab-rejected" class="tab-content">
<h2>❌ Abgelehnte / Archivierte Vorschläge</h2>
<div class="questions-list">
{% for q in rejected_questions %}
<div class="card question-admin-card muted-card" id="q-card-{{ q.id }}">
<div class="q-header">
<span class="badge badge-danger">Abgelehnt</span>
<span class="q-submitter">Eingereicht von: {{ q.submitted_by or 'Gast' }}</span>
</div>
<div class="q-body">
<h3>{{ q.question }}</h3>
<p><strong>Antwort:</strong> {{ q.canonical_answer }}</p>
</div>
<div class="q-actions">
<button class="btn btn-sm btn-success" onclick="approveQuestion({{ q.id }})">Wieder aktivieren</button>
<button class="btn btn-sm btn-danger" onclick="deleteQuestion({{ q.id }})">Endgültig löschen</button>
</div>
</div>
{% endfor %}
</div>
</section>
{% endif %}
</div>
{% endblock %}
{% block scripts %}
<script src="/static/js/admin.js"></script>
{% endblock %}
+34
View File
@@ -0,0 +1,34 @@
{% extends "base.html" %}
{% block title %}🔒 Zaubereiministerium – Login{% endblock %}
{% block content %}
<div class="quiz-wrapper">
<div class="card login-card">
<h1 class="glow-title">🔒 Zaubereiministerium</h1>
<p class="subtitle">Nur für befugte Großinquisitoren & Administratoren</p>
{% if error %}
<div class="alert alert-danger">
{{ error }}
</div>
{% endif %}
<form action="/admin/login" method="post" class="magic-form">
<div class="form-group">
<label for="username">Zauberer-Name:</label>
<input type="text" id="username" name="username" class="input-text" required autofocus>
</div>
<div class="form-group">
<label for="password">Geheimer Zauberspruch (Passwort):</label>
<input type="password" id="password" name="password" class="input-text" required>
</div>
<button type="submit" class="btn btn-primary btn-large">
Alohomora (Einloggen)
</button>
</form>
</div>
</div>
{% endblock %}
+69
View File
@@ -0,0 +1,69 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Magisches Harry-Potter-Escape-Quiz{% endblock %}</title>
<link rel="icon" href="/static/favicon.ico">
<link rel="stylesheet" href="/static/css/style.css">
{% block head %}{% endblock %}
</head>
<body>
<audio id="bgm" src="/static/audio/music.mp3" loop preload="auto"></audio>
<div class="audio-control-bar">
<button id="audioToggle" class="audio-btn" title="Musik an/aus" onclick="toggleAudio()">
<span id="audioIcon">🔇</span>
<span id="audioText">Musik: Aus</span>
</button>
<div class="nav-links">
<a href="/" class="nav-link">✨ Zum Quiz</a>
<a href="/vorschlag" class="nav-link">✍️ Frage einreichen</a>
<a href="/admin" class="nav-link">🔒 Zaubereiministerium (Admin)</a>
</div>
</div>
<main class="container">
{% block content %}{% endblock %}
</main>
<footer class="footer">
<p>⚡ Magisches Hogwarts-Escape-Quiz · Betrieben mit Liebe & Zauberkraft ⚡</p>
</footer>
<script>
const bgm = document.getElementById("bgm");
const audioIcon = document.getElementById("audioIcon");
const audioText = document.getElementById("audioText");
function toggleAudio() {
if (bgm.paused) {
bgm.play().then(() => {
audioIcon.innerText = "🔊";
audioText.innerText = "Musik: An";
localStorage.setItem("bgm_enabled", "true");
}).catch(err => {
console.log("Audio play blocked:", err);
});
} else {
bgm.pause();
audioIcon.innerText = "🔇";
audioText.innerText = "Musik: Aus";
localStorage.setItem("bgm_enabled", "false");
}
}
// Auto-resume audio if previously enabled
document.addEventListener("click", function initAudioOnInteraction() {
if (localStorage.getItem("bgm_enabled") === "true" && bgm.paused) {
bgm.play().then(() => {
audioIcon.innerText = "🔊";
audioText.innerText = "Musik: An";
}).catch(() => {});
}
document.removeEventListener("click", initAudioOnInteraction);
}, { once: true });
</script>
{% block scripts %}{% endblock %}
</body>
</html>
+35
View File
@@ -0,0 +1,35 @@
{% extends "base.html" %}
{% block title %}🎉 Das große Finale – Magisches Quiz{% endblock %}
{% block content %}
<div class="quiz-wrapper">
<div class="card finale-card">
<div class="finale-header">
<span class="sparkle-icon">✨</span>
<h1 class="glow-title">🎉 Happy Birthday Schatz! 🎉</h1>
<span class="sparkle-icon">✨</span>
</div>
<div class="score-banner">
<p class="score-highlight">
Du hast <span class="gold-text">{{ score }}</span> von <span class="gold-text">{{ total }}</span> möglichen Hauspunkten für dein Haus gesichert!
</p>
</div>
<div class="reward-box">
<h2>💛 Dein magischer Gewinn: Ein unvergesslicher Abend Zuhause 💛</h2>
<p class="reward-description">
Wir holen uns lecker Essen und machen es uns so richtig gemütlich: entspannen, lachen und die gemeinsame Zeit genießen.<br><br>
Dazu spielen wir ein brandneues <strong>Online Escape Game</strong> deiner Wahl!
</p>
<p class="love-note">Ich freue mich riesig auf unseren gemeinsamen Abend! 💫</p>
</div>
<div class="btn-group finale-actions">
<a href="/" class="btn btn-primary">🔄 Erneut spielen</a>
<a href="/vorschlag" class="btn btn-secondary">✍️ Eigene Frage vorschlagen</a>
</div>
</div>
</div>
{% endblock %}
+82
View File
@@ -0,0 +1,82 @@
{% extends "base.html" %}
{% block title %}✨ Magisches Harry-Potter-Escape-Quiz ✨{% endblock %}
{% block content %}
<div class="quiz-wrapper">
<header class="quiz-header">
<h1 class="glow-title">✨ Magisches Hogwarts-Escape-Quiz ✨</h1>
<p class="subtitle">15 knifflige Rätsel aus der Zauberwelt warten auf dich</p>
</header>
<!-- Brief-Intro / Startmaske -->
<div id="introCard" class="card intro-card">
<div class="intro-envelope" id="envelope">
<div class="envelope-body">
<div class="envelope-flap"></div>
<div class="wax-seal">H</div>
<div class="letter-preview">
<p class="letter-text">Hogwarts-Schule für Hexerei und Zauberei</p>
</div>
</div>
</div>
<div class="intro-content">
<p class="intro-text">
Ein verzauberter Brief aus Hogwarts schwebt vor dir in der Luft.<br>
Bist du bereit, die Prüfungen abzulegen und deine Hauspunkte zu sichern?
</p>
<button id="startBtn" class="btn btn-primary btn-large" onclick="startGame()">
📜 Siegel brechen & Brief öffnen
</button>
</div>
</div>
<!-- Spielbereich (initial versteckt) -->
<div id="gameCard" class="game-area hidden">
<!-- Fortschritt & Punkte -->
<div class="status-panel">
<div class="progress-info">
<span id="questionCounter">Frage 1 von {{ question_count }}</span>
<span id="pointsCounter" class="points-badge">🏆 Hauspunkte: 0</span>
</div>
<div class="progress-container">
<div id="progressBar" class="progress-bar"></div>
</div>
</div>
<!-- Frage-Container -->
<div id="frageBox" class="card question-card">
<div class="question-meta">
<span id="badgeCategory" class="badge">⚡ Harry Potter</span>
<span id="attemptBadge" class="attempt-badge">Versuch 1 von 3</span>
</div>
<h2 id="frageText" class="question-text">Lade verzauberte Schriftrollen...</h2>
<div class="answer-form">
<input type="text" id="answerInput" class="input-answer" placeholder="Deine Antwort hier eingeben..." autocomplete="off">
<div class="btn-group">
<button id="checkBtn" class="btn btn-primary" onclick="checkAnswer()">
✨ Zauber prüfen
</button>
<button id="hintBtn" class="btn btn-secondary" onclick="requestHint()">
💡 Zaubertipp
</button>
</div>
</div>
<div id="hintBox" class="hint-box hidden">
<strong>📜 Notiz aus der verbotenen Abteilung:</strong>
<p id="hintText"></p>
</div>
<div id="feedbackBox" class="feedback-box"></div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="/static/js/quiz.js"></script>
{% endblock %}
+49
View File
@@ -0,0 +1,49 @@
{% extends "base.html" %}
{% block title %}✍️ Neue Zauberfrage einreichen{% endblock %}
{% block content %}
<div class="quiz-wrapper">
<div class="card vorschlag-card">
<h1 class="glow-title">✨ Neue Frage für das Quiz vorschlagen ✨</h1>
<p class="subtitle">Bereichere das magische Quiz mit kniffligen Harry-Potter- oder Escape-Rätseln!</p>
<div class="info-alert">
ℹ️ <strong>Hinweis zur Moderation:</strong> Eingereichte Fragen werden nicht sofort öffentlich sichtbar, sondern erhalten automatisch den Status <em>„Prüfung ausstehend“</em>. Unser Zaubereiministerium prüft und testet die Antwort, bevor die Frage freigeschaltet wird.
</div>
<form id="suggestionForm" class="magic-form" onsubmit="submitSuggestion(event)">
<div class="form-group">
<label for="name">Dein Name / Zauberer-Alias (optional):</label>
<input type="text" id="name" name="name" class="input-text" placeholder="z. B. Hermine Granger">
</div>
<div class="form-group">
<label for="frage">Deine Frage: <span class="required">*</span></label>
<textarea id="frage" name="frage" class="textarea" rows="3" placeholder="z. B. Wie heißt der Schulleiter von Hogwarts?" required></textarea>
</div>
<div class="form-group">
<label for="antwort">Richtige Antwort: <span class="required">*</span></label>
<input type="text" id="antwort" name="antwort" class="input-text" placeholder="z. B. Albus Dumbledore" required>
<small class="field-help">Unser System gleicht Tippfehler und Groß-/Kleinschreibung automatisch ab.</small>
</div>
<div class="form-group">
<label for="tipp">Tipp / Hinweis (optional):</label>
<input type="text" id="tipp" name="tipp" class="input-text" placeholder="z. B. Er trägt eine Halbmondbrille.">
</div>
<button type="submit" id="submitBtn" class="btn btn-primary btn-large">
📜 Vorschlag an das Ministerium senden
</button>
</form>
<div id="resultMessage" class="feedback-box hidden"></div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="/static/js/vorschlag.js"></script>
{% endblock %}