Compare commits

...

2 Commits

Author SHA1 Message Date
kobold-ai 6c62676d0d feat: Add lore pack, UI adjustments, requirements.txt and INSTALL.md 2026-10-29 08:51:28 +01:00
kobold-ai fb789e8866 Add Escape Adventure navigation link to bootle.dev.waiswuaschd.de 2026-09-22 16:27:10 +02:00
14 changed files with 3281 additions and 2723 deletions
+89
View File
@@ -0,0 +1,89 @@
# Magisches Harry Potter Escape Quiz Portal
Interaktives Harry Potter Escape Quiz Portal (FastAPI & MariaDB).
## Systemanforderungen
- Debian 12 / 13 (oder kompatibles Linux)
- Python 3.13+ mit `python3-venv`
- Nginx (Reverse Proxy mit SSL)
- MariaDB Server auf \`db.waiswuaschd.de:3306\` (Datenbank: \`v2_db_quiz\`)
## 1. Installation
```bash
# Repository klonen
cd /home/maxi/Projekt
git clone https://gitea.sp13l.de/kobold-ai/quiz.git quiz.waiswuaschd.de
cd quiz.waiswuaschd.de
# Python Virtualenv erstellen und Abh?ngigkeiten installieren
python3 -m venv venv
./venv/bin/pip install --upgrade pip
./venv/bin/pip install -r requirements.txt
```
## 2. Umgebungsvariablen (.env)
Erstelle im Projektverzeichnis eine `.env` Datei:
```ini
DB_HOST=db.waiswuaschd.de
DB_PORT=3306
DB_NAME=v2_db_quiz
DB_USER=v2_usr_quiz
DB_PASS=Quiz_Pass_2026_Secure!
SECRET_KEY=hogwarts_magic_super_secret_key_2026_dev_quiz
ADMIN_USER=admin
ADMIN_PASS=Alohomora2026!
PORT=8001
```
## 3. Systemd Service (/etc/systemd/system/quiz.service)
```ini
[Unit]
Description=Magisches Quiz Portal (FastAPI)
After=network.target
[Service]
User=maxi
Group=maxi
WorkingDirectory=/home/maxi/Projekt/quiz.waiswuaschd.de
ExecStart=/home/maxi/Projekt/quiz.waiswuaschd.de/venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 8001 --workers 1
Restart=always
RestartSec=5
Environment=PYTHONUNBUFFERED=1
[Install]
WantedBy=multi-user.target
```
Aktivieren und starten:
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now quiz
```
## 4. Nginx Reverse Proxy
```nginx
server {
listen 80;
server_name quiz.dev.waiswuaschd.de;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name quiz.dev.waiswuaschd.de;
# SSL Zertifikate hier konfigurieren (z. B. via certbot)
location /static/ {
alias /home/maxi/Projekt/quiz.waiswuaschd.de/app/static/;
add_header Cache-Control "public, max-age=3600, must-revalidate";
}
location / {
proxy_pass http://127.0.0.1:8001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
+26 -5
View File
@@ -20,7 +20,7 @@ from app.schemas import GameCheckRequest, SuggestionCreateRequest, GameStartRequ
load_dotenv()
app = FastAPI(title="Magisches Harry-Potter-Escape-Quiz")
app = FastAPI(title="PotterTrivia – Das Harry-Potter-Wissensquiz")
SECRET_KEY = os.getenv("SECRET_KEY", "hogwarts_secret_default_key_2026")
app.add_middleware(SessionMiddleware, secret_key=SECRET_KEY, session_cookie="quiz_session")
@@ -154,16 +154,37 @@ async def index(request: Request, db: Session = Depends(get_db)):
context={"question_count": active_count}
)
VALID_CATEGORIES = ["all", "books", "movies", "creatures", "spells", "characters", "hogwarts", "quidditch", "lore", "general"]
@app.post("/api/game/start")
async def start_game(payload: GameStartRequest = None, db: Session = Depends(get_db)):
lang = payload.lang if payload and payload.lang in ["de", "en"] else "de"
difficulty = payload.difficulty if payload and payload.difficulty in ["easy", "medium", "hard"] else "all"
category = payload.category if payload and payload.category in VALID_CATEGORIES else "all"
base_query = db.query(Question).filter(Question.status == "active")
if category != "all":
if category == "books":
cat_query = base_query.filter((Question.category == "books") | (Question.question.like("%Kröter%")))
else:
cat_query = base_query.filter(Question.category == category)
else:
cat_query = base_query
query = db.query(Question).filter(Question.status == "active")
if difficulty != "all":
query = query.filter(Question.difficulty == difficulty)
strict_pool = cat_query.filter(Question.difficulty == difficulty).all()
if len(strict_pool) >= 10 or category == "all":
pool = strict_pool
else:
# Keep all difficulty matches in this category and top up from the same category
cat_all = cat_query.all()
strict_ids = {q.id for q in strict_pool}
extra = [q for q in cat_all if q.id not in strict_ids]
random.shuffle(extra)
pool = strict_pool + extra[:max(0, 10 - len(strict_pool))]
else:
pool = cat_query.all()
pool = query.all()
if not pool:
pool = db.query(Question).filter(Question.status == "active").all()
if not pool:
@@ -538,7 +559,7 @@ async def submit_suggestion(payload: SuggestionCreateRequest, db: Session = Depe
opts.append(opt.strip())
options_json = json.dumps(opts, ensure_ascii=False)
chosen_cat = payload.category if payload.category in ["hogwarts", "characters", "spells", "creatures", "quidditch", "lore", "general"] else "general"
chosen_cat = payload.category if payload.category in ["books", "movies", "hogwarts", "characters", "spells", "creatures", "quidditch", "lore", "general"] else "general"
diff = payload.difficulty if payload.difficulty in ["easy", "medium", "hard"] else "medium"
new_suggestion = Question(
+18
View File
@@ -115,4 +115,22 @@ def is_answer_correct(user_input: str, canonical: str, accepted_raw: str | None
if dist <= max_allowed:
return True
# Support compound answers connected by 'und' / 'and' (e.g. "Mantikor und Feuerkrabbe")
for sep in (" und ", " and "):
if sep in target:
parts = [p.strip() for p in target.split(sep) if len(p.strip()) >= 3]
if len(parts) == 2:
u_words = u.split()
matched_both = True
for part in parts:
if part in u:
continue
# Check fuzzy match against tokens or reversed order
part_matched = any(levenshtein_distance(w, part) <= (1 if len(part) <= 6 else 2) for w in u_words)
if not part_matched:
matched_both = False
break
if matched_both:
return True
return False
+1
View File
@@ -4,6 +4,7 @@ from typing import Optional, List
class GameStartRequest(BaseModel):
lang: Optional[str] = "de"
difficulty: Optional[str] = "all"
category: Optional[str] = "all"
recent_ids: Optional[List[int]] = None
wrong_ids: Optional[List[int]] = None
+4
View File
@@ -4,6 +4,7 @@ from app.database import engine, SessionLocal, Base
from app.models import Question, AdminUser
from app.auth import hash_password
from app.seed_40 import seed_40_questions
from app.seed_lore_pack import seed_lore_pack
def seed_db():
Base.metadata.create_all(bind=engine)
@@ -29,6 +30,9 @@ def seed_db():
print("Question table is empty, seeding 40 initial bilingual questions...")
seed_40_questions()
# 3. Ensure Lore Pack (Books, Movies, Hard Creatures incl. Blast-Ended Skrewts) is present
seed_lore_pack()
finally:
db.close()
+722
View File
@@ -0,0 +1,722 @@
import json
from app.database import SessionLocal
from app.models import Question
LORE_PACK_QUESTIONS = [
# --- CREATURES (Magische Geschöpfe) ---
{
"category": "creatures",
"difficulty": "hard",
"question_type": "text",
"question": "Aus welcher Kreuzung zweier magischer Geschöpfe züchtete Hagrid die Knallrümpfigen Kröter?",
"canonical_answer": "Mantikor und Feuerkrabbe",
"accepted_answers": json.dumps([
"Mantikor und Feuerkrabbe",
"Feuerkrabbe und Mantikor",
"Mantikor Feuerkrabbe",
"Feuerkrabbe Mantikor",
"Manticore and Fire Crab",
"Fire Crab and Manticore"
], ensure_ascii=False),
"hint": "Eines ist ein hochgefährliches griechisches Ungeheuer mit Skorpionschwanz, das andere ein schildkrötenähnliches Wesen von den Fidschi-Inseln.",
"question_en": "Which two magical creatures did Hagrid crossbreed to create the Blast-Ended Skrewts?",
"answer_en": "Manticore and Fire Crab",
"hint_en": "One is a deadly Greek beast with a scorpion tail, the other is a turtle-like creature native to Fiji."
},
{
"category": "creatures",
"difficulty": "hard",
"question_type": "multiple_choice",
"question": "Welcher der drei Köpfe der dreiköpfigen Schlange 'Runespoor' ist laut Newt Scamander derjenige, der Pläne schmiedet?",
"canonical_answer": "Der linke Kopf",
"options": json.dumps(["Der linke Kopf", "Der mittlere Kopf", "Der rechte Kopf", "Alle drei gleichzeitig"], ensure_ascii=False),
"hint": "Der mittlere Kopf ist der Träumer und der rechte Kopf der Kritiker.",
"question_en": "According to Newt Scamander, which of the three heads of the Runespoor snake is the planner?",
"answer_en": "The left head",
"options_en": json.dumps(["The left head", "The middle head", "The right head", "All three simultaneously"], ensure_ascii=False),
"hint_en": "The middle head is the dreamer and the right head is the critic."
},
{
"category": "creatures",
"difficulty": "medium",
"question_type": "text",
"question": "Wie heißt die riesige Spinne (Acromantula), die Hagrid schon während seiner Schulzeit in einem Schrank in Hogwarts versteckte?",
"canonical_answer": "Aragog",
"accepted_answers": json.dumps(["Aragog"], ensure_ascii=False),
"hint": "Sein Name beginnt mit 'A' und er lebt später mit seiner großen Kolonie im Verbotenen Wald.",
"question_en": "What is the name of the giant Acromantula spider that Hagrid hid in a cupboard at Hogwarts during his school days?",
"answer_en": "Aragog",
"hint_en": "His name starts with 'A' and he later lives with his large colony in the Forbidden Forest."
},
{
"category": "creatures",
"difficulty": "hard",
"question_type": "multiple_choice",
"question": "Welche Gefahrenklasse (Ministeriumsklassifizierung) hat ein ausgewachsener Nundu laut 'Phantastische Tierwesen'?",
"canonical_answer": "XXXXX (Bekannte Zauberertöter)",
"options": json.dumps([
"XXXXX (Bekannte Zauberertöter)",
"XXXX (Gefährlich / erfordert Fachwissen)",
"XXX (Für fähige Zauberer kein Problem)",
"XX (Harmlos / kann gezähmt werden)"
], ensure_ascii=False),
"hint": "Es braucht mindestens hundert zusammenarbeitende Zauberer, um einen einzigen Nundu zu überwältigen.",
"question_en": "What Ministry of Magic classification does an adult Nundu hold according to 'Fantastic Beasts'?",
"answer_en": "XXXXX (Known wizard killer)",
"options_en": json.dumps([
"XXXXX (Known wizard killer)",
"XXXX (Dangerous / requires specialist knowledge)",
"XXX (Competent wizard should cope)",
"XX (Harmless / may be domesticated)"
], ensure_ascii=False),
"hint_en": "It takes at least a hundred skilled wizards working together to subdue a single Nundu."
},
{
"category": "creatures",
"difficulty": "easy",
"question_type": "text",
"question": "Welches kleine, pelzige Tierwesen wird magisch von allem Glitzernden und von Goldmünzen angezogen?",
"canonical_answer": "Niffler",
"accepted_answers": json.dumps(["Niffler", "Ein Niffler"], ensure_ascii=False),
"hint": "Newt Scamander hat eines dieser langnasigen Beuteltiere in seinem Koffer.",
"question_en": "Which small, furry magical creature is irresistibly drawn to anything shiny and gold coins?",
"answer_en": "Niffler",
"hint_en": "Newt Scamander keeps one of these long-snouted burrowing creatures in his case."
},
{
"category": "creatures",
"difficulty": "medium",
"question_type": "multiple_choice",
"question": "Wie nennt man das unsichtbare geflügelte Pferd, das nur von Menschen gesehen werden kann, die den Tod bereits bewusst miterlebt haben?",
"canonical_answer": "Thestral",
"options": json.dumps(["Thestral", "Hippogreif", "Abraxaner", "Kelpie"], ensure_ascii=False),
"hint": "Sie ziehen die Kutschen vom Bahnhof Hogsmeade zum Schloss Hogwarts.",
"question_en": "What is the name of the invisible winged horse that can only be seen by those who have witnessed death?",
"answer_en": "Thestral",
"options_en": json.dumps(["Thestral", "Hippogriff", "Abraxan", "Kelpie"], ensure_ascii=False),
"hint_en": "They pull the carriages from Hogsmeade station up to Hogwarts Castle."
},
# --- BOOKS (Buchwissen) ---
{
"category": "books",
"difficulty": "hard",
"question_type": "text",
"question": "Wie lautet das vollständige Akronym der Organisation, die Hermine Granger im 4. Buch zur Befreiung der Hauselfen gründet?",
"canonical_answer": "B.ELFE.R.",
"accepted_answers": json.dumps(["B.ELFE.R.", "BELFER", "B.ELFE.R", "S.P.E.W.", "SPEW", "Bund für Elfenrechte"]),
"hint": "Es steht für 'Bund für Elfenrechte' (im Englischen: S.P.E.W.).",
"question_en": "What is the acronym of the organization Hermione Granger founds in her fourth year to campaign for house-elf rights?",
"answer_en": "S.P.E.W.",
"hint_en": "It stands for 'Society for the Promotion of Elfish Welfare' (in German: B.ELFE.R.)."
},
{
"category": "books",
"difficulty": "hard",
"question_type": "multiple_choice",
"question": "Wer schenkt Harry im 4. Buch (im Gegensatz zum Film!) das Kraut 'Dianthuskraut' (Gillyweed) für die zweite Aufgabe des Trimagischen Turniers?",
"canonical_answer": "Dobby",
"options": json.dumps(["Dobby", "Neville Longbottom", "Alastor Moody", "Cedric Diggory"], ensure_ascii=False),
"hint": "Er stiehlt es aus Professor Snapes Vorratskammer, nachdem er ein inszeniertes Gespräch von Barty Crouch Jr. belauscht hat.",
"question_en": "In the Goblet of Fire book (unlike the movie!), who actually gives Harry the Gillyweed before the Second Task?",
"answer_en": "Dobby",
"options_en": json.dumps(["Dobby", "Neville Longbottom", "Alastor Moody", "Cedric Diggory"], ensure_ascii=False),
"hint_en": "He steals it from Professor Snape's private stores after overhearing a staged conversation by Barty Crouch Jr."
},
{
"category": "books",
"difficulty": "hard",
"question_type": "text",
"question": "Wie heißt der Poltergeist von Hogwarts, der in allen sieben Büchern für Chaos sorgt, aber aus den Verfilmungen komplett gestrichen wurde?",
"canonical_answer": "Peeves",
"accepted_answers": json.dumps(["Peeves", "Poltergeist Peeves"]),
"hint": "Nur der Blutige Baron hat ihn wirklich unter Kontrolle.",
"question_en": "What is the name of the Hogwarts poltergeist who causes chaos throughout all seven books but was completely cut from the films?",
"answer_en": "Peeves",
"hint_en": "Only the Bloody Baron can truly keep him in line."
},
{
"category": "books",
"difficulty": "hard",
"question_type": "multiple_choice",
"question": "Welches Rätsel stellt Professor Snape in Band 1 als Schutz vor dem Stein der Weisen auf, das im ersten Kinofilm komplett fehlt?",
"canonical_answer": "Ein Logikrätsel mit sieben Zaubertrank-Flaschen",
"options": json.dumps([
"Ein Logikrätsel mit sieben Zaubertrank-Flaschen",
"Ein Duell gegen eine verzauberte Rüstung",
"Ein Schachbrett aus flüssigem Quecksilber",
"Eine Tür, die nur auf Parsel reagiert"
], ensure_ascii=False),
"hint": "Hermine löst es mit reiner Logik, während ein schwarzes und ein violettes Feuer die Ausgänge blockieren.",
"question_en": "Which obstacle guarding the Philosopher's Stone was created by Professor Snape in the first book, but omitted from the film?",
"answer_en": "A logic puzzle with seven potion bottles",
"options_en": json.dumps([
"A logic puzzle with seven potion bottles",
"A duel against an enchanted suit of armor",
"A chessboard made of liquid mercury",
"A door that only opens to Parseltongue"
], ensure_ascii=False),
"hint_en": "Hermione solves it using pure logic while black and purple flames block the exits."
},
{
"category": "books",
"difficulty": "hard",
"question_type": "text",
"question": "Wie heißt die Hauselfe der Familie Barty Crouch, die beim Quidditch-Weltpokal mit Harrys Zauberstab in der Hand gefunden wird?",
"canonical_answer": "Winky",
"accepted_answers": json.dumps(["Winky", "Hauselfe Winky"]),
"hint": "Ihr Name beginnt mit 'W' und sie verfällt nach ihrer Entlassung dem Butterbier.",
"question_en": "What is the name of the Crouch family's female house-elf who is found holding Harry's wand at the Quidditch World Cup?",
"answer_en": "Winky",
"hint_en": "Her name starts with 'W' and she later develops a Butterbeer habit in the Hogwarts kitchens."
},
{
"category": "books",
"difficulty": "hard",
"question_type": "multiple_choice",
"question": "Wie heißt Tom Riddles Mutter mit vollem Geburtsnamen, deren tragische Geschichte Harry in Dumbledores Denkarium im 6. Buch erfährt?",
"canonical_answer": "Merope Gaunt",
"options": json.dumps(["Merope Gaunt", "Morfin Gaunt", "Kendra Dumbledore", "Eileen Prince"], ensure_ascii=False),
"hint": "Sie war eine Nachfahrin von Salazar Slytherin und verliebte sich in den Muggel Tom Riddle Senior.",
"question_en": "What was the maiden name of Tom Riddle's mother, whose tragic story Harry learns in Dumbledore's Pensieve in book 6?",
"answer_en": "Merope Gaunt",
"options_en": json.dumps(["Merope Gaunt", "Morfin Gaunt", "Kendra Dumbledore", "Eileen Prince"], ensure_ascii=False),
"hint_en": "She was a direct descendant of Salazar Slytherin and fell in love with the Muggle Tom Riddle Sr."
},
{
"category": "books",
"difficulty": "medium",
"question_type": "multiple_choice",
"question": "Auf welcher Krankenstation im St.-Mungo-Hospital begegnen Harry, Ron und Hermine im 5. Buch zufällig Gilderoy Lockhart wieder?",
"canonical_answer": "Janus-Thickey-Station (Langzeit-Fluchschäden)",
"options": json.dumps([
"Janus-Thickey-Station (Langzeit-Fluchschäden)",
"Dai-Llewellyn-Station (Tierbisse)",
"Artefakt-Unfälle im Erdgeschoss",
"Teekessel-Verbrennungsstation"
], ensure_ascii=False),
"hint": "Auf derselben geschlossenen Station im 4. Stock liegen auch Frank und Alice Longbottom.",
"question_en": "On which ward of St Mungo's Hospital do Harry, Ron, and Hermione bump into Gilderoy Lockhart in the fifth book?",
"answer_en": "Janus Thickey Ward (Long-Term Spell Damage)",
"options_en": json.dumps([
"Janus Thickey Ward (Long-Term Spell Damage)",
"Dai Llewellyn Ward (Creature Bites)",
"Artifact Accidents on the Ground Floor",
"Potion and Plant Poisoning Ward"
], ensure_ascii=False),
"hint_en": "Frank and Alice Longbottom are patients on the very same fourth-floor ward."
},
{
"category": "books",
"difficulty": "hard",
"question_type": "text",
"question": "Welchen Beruf übt Charlie Weasley in Rumänien aus?",
"canonical_answer": "Drachenwärter",
"accepted_answers": json.dumps([
"Drachenwärter", "Drachenforscher", "Drachenzüchter", "Drachenpfleger",
"Er arbeitet mit Drachen", "Drachen", "Dragonologist", "Dragon keeper"
], ensure_ascii=False),
"hint": "Er nimmt im 1. Buch den kleinen Norwegischen Stachelbuckel Norbert in seine Obhut.",
"question_en": "What is Charlie Weasley's profession in Romania?",
"answer_en": "Dragonkeeper",
"hint_en": "In the first book, his friends fly down to take Hagrid's Norwegian Ridgeback Norbert into their care."
},
{
"category": "books",
"difficulty": "medium",
"question_type": "multiple_choice",
"question": "Was tut Harry im Buch 'Die Heiligtümer des Todes' mit dem Elderstab, bevor er ihn an seinen endgültigen Ort zurückbringt?",
"canonical_answer": "Er repariert seinen eigenen zerbrochenen Zauberstab",
"options": json.dumps([
"Er repariert seinen eigenen zerbrochenen Zauberstab",
"Er zerbricht ihn und wirft ihn von der Brücke",
"Er schenkt ihn Ron Weasley",
"Er übergibt ihn dem Zaubereiministerium"
], ensure_ascii=False),
"hint": "Mit 'Reparo' gelingt ihm etwas, was Ollivander zuvor für unmöglich gehalten hatte.",
"question_en": "In the Deathly Hallows book, what does Harry do with the Elder Wand before returning it to Dumbledore's tomb?",
"answer_en": "He repairs his own broken holly and phoenix wand",
"options_en": json.dumps([
"He repairs his own broken holly and phoenix wand",
"He snaps it in half and throws it off the bridge",
"He gifts it to Ron Weasley",
"He locks it inside Gringotts"
], ensure_ascii=False),
"hint_en": "Using 'Reparo', he accomplishes something Ollivander thought was impossible."
},
{
"category": "books",
"difficulty": "hard",
"question_type": "multiple_choice",
"question": "Wie heißt der Kobold, der im 4. Buch von Ludo Bagman mit Leprechaun-Gold betrogen wird?",
"canonical_answer": "Gornuk (bzw. Ragnok / Griphooks Kollegen)",
"options": json.dumps(["Ragnok", "Bogrod", "Travers", "Yaxley"], ensure_ascii=False),
"hint": "Er trifft sich im Eberkopf mit Ludo Bagman, um seine Schulden einzufordern.",
"question_en": "Which goblin confronts Ludo Bagman in the Hog's Head inn after being paid in vanishing Leprechaun gold?",
"answer_en": "Ragnok",
"options_en": json.dumps(["Ragnok", "Bogrod", "Travers", "Yaxley"], ensure_ascii=False),
"hint_en": "He meets Ludo Bagman in the Hog's Head to demand repayment of his bet."
},
{
"category": "books",
"difficulty": "medium",
"question_type": "text",
"question": "Welches magische Radio-Programm hören Ron, Harry und Hermine im 7. Buch auf der Flucht, für das man ein wechselndes Passwort braucht?",
"canonical_answer": "PotterWatch",
"accepted_answers": json.dumps(["PotterWatch", "Potter Watch", "Potterwatch"]),
"hint": "Moderiert wird die Sendung von Lee Jordan unter dem Decknamen 'Fluss' (River).",
"question_en": "What is the name of the secret rebel radio broadcast that Harry, Ron, and Hermione listen to while on the run in book 7?",
"answer_en": "Potterwatch",
"hint_en": "The program is hosted by Lee Jordan under the codename 'River'."
},
{
"category": "books",
"difficulty": "hard",
"question_type": "multiple_choice",
"question": "Wer war die ursprüngliche Besitzerin des Diadems von Ravenclaw und zugleich deren Tochter (die Graue Dame)?",
"canonical_answer": "Helena Ravenclaw",
"options": json.dumps(["Helena Ravenclaw", "Rowena Ravenclaw", "Helga Hufflepuff", "Millicent Bagnold"], ensure_ascii=False),
"hint": "Sie stahl das Diadem ihrer Mutter und versteckte es in einem hohlen Baum in Albanien.",
"question_en": "What is the real name of the Grey Lady, the ghost of Ravenclaw Tower and daughter of Rowena Ravenclaw?",
"answer_en": "Helena Ravenclaw",
"options_en": json.dumps(["Helena Ravenclaw", "Rowena Ravenclaw", "Helga Hufflepuff", "Millicent Bagnold"], ensure_ascii=False),
"hint_en": "She stole her mother's diadem and hid it inside a hollow tree in Albania."
},
# --- MOVIES (Filmwissen) ---
{
"category": "movies",
"difficulty": "easy",
"question_type": "text",
"question": "Welcher britische Schauspieler verkörperte Harry Potter in allen acht Kinofilmen?",
"canonical_answer": "Daniel Radcliffe",
"accepted_answers": json.dumps(["Daniel Radcliffe", "Radcliffe"]),
"hint": "Er war bei den Dreharbeiten zum ersten Film erst elf Jahre alt.",
"question_en": "Which British actor portrayed Harry Potter across all eight feature films?",
"answer_en": "Daniel Radcliffe",
"hint_en": "He was only eleven years old when filming began on the first movie."
},
{
"category": "movies",
"difficulty": "medium",
"question_type": "multiple_choice",
"question": "Welcher Regisseur inszenierte den dritten Kinofilm 'Harry Potter und der Gefangene von Askaban' (2004)?",
"canonical_answer": "Alfonso Cuarón",
"options": json.dumps(["Alfonso Cuarón", "Chris Columbus", "David Yates", "Mike Newell"], ensure_ascii=False),
"hint": "Der mexikanische Regisseur prägte den düsteren, künstlerischen Look des dritten Teils.",
"question_en": "Which director helmed the third movie, 'Harry Potter and the Prisoner of Azkaban' (2004)?",
"answer_en": "Alfonso Cuarón",
"options_en": json.dumps(["Alfonso Cuarón", "Chris Columbus", "David Yates", "Mike Newell"], ensure_ascii=False),
"hint_en": "The Mexican filmmaker gave the third installment its signature darker, atmospheric visual style."
},
{
"category": "movies",
"difficulty": "medium",
"question_type": "text",
"question": "Welcher weltberühmte Filmkomponist schrieb das ikonische Hauptthema 'Hedwig's Theme' für die ersten Harry-Potter-Filme?",
"canonical_answer": "John Williams",
"accepted_answers": json.dumps(["John Williams", "Williams"]),
"hint": "Er komponierte auch die Musik für Star Wars, Indiana Jones und Jurassic Park.",
"question_en": "Which legendary film composer wrote the iconic main theme 'Hedwig's Theme' for the first Harry Potter movies?",
"answer_en": "John Williams",
"hint_en": "He also composed the scores for Star Wars, Indiana Jones, and Jurassic Park."
},
{
"category": "movies",
"difficulty": "medium",
"question_type": "multiple_choice",
"question": "Welcher Schauspieler übernahm ab dem dritten Film die Rolle von Albus Dumbledore, nachdem Richard Harris verstorben war?",
"canonical_answer": "Michael Gambon",
"options": json.dumps(["Michael Gambon", "Ian McKellen", "Gary Oldman", "Ralph Fiennes"], ensure_ascii=False),
"hint": "Er spielte Dumbledore vom dritten bis zum achten Film.",
"question_en": "Which actor took over the role of Albus Dumbledore starting in the third film after Richard Harris passed away?",
"answer_en": "Michael Gambon",
"options_en": json.dumps(["Michael Gambon", "Ian McKellen", "Gary Oldman", "Ralph Fiennes"], ensure_ascii=False),
"hint_en": "He portrayed Dumbledore from Prisoner of Azkaban through Deathly Hallows – Part 2."
},
{
"category": "movies",
"difficulty": "easy",
"question_type": "multiple_choice",
"question": "Über welches berühmte schottische Eisenbahnviadukt fährt der Hogwarts-Express in den Filmen?",
"canonical_answer": "Glenfinnan-Viadukt",
"options": json.dumps(["Glenfinnan-Viadukt", "Ribblehead-Viadukt", "Clifton-Brücke", "Forth Bridge"], ensure_ascii=False),
"hint": "Es liegt in den schottischen Highlands am Loch Shiel.",
"question_en": "Which famous Scottish railway viaduct does the Hogwarts Express cross in the movies?",
"answer_en": "Glenfinnan Viaduct",
"options_en": json.dumps(["Glenfinnan Viaduct", "Ribblehead Viaduct", "Clifton Bridge", "Forth Bridge"], ensure_ascii=False),
"hint_en": "It is located in the Scottish Highlands overlooking Loch Shiel."
},
{
"category": "movies",
"difficulty": "hard",
"question_type": "multiple_choice",
"question": "Welche Augenfarbe hat Harry Potter in den Kinofilmen (weil Daniel Radcliffe die farbigen Kontaktlinsen nicht vertrug), obwohl sie in den Büchern leuchtend grün sind?",
"canonical_answer": "Blau",
"options": json.dumps(["Blau", "Braun", "Grau", "Bernsteinfarben"], ensure_ascii=False),
"hint": "Daniel Radcliffes natürliche Augenfarbe.",
"question_en": "What color are Harry Potter's eyes in the movies (because Daniel Radcliffe had an allergic reaction to contact lenses), even though they are bright green in the books?",
"answer_en": "Blue",
"options_en": json.dumps(["Blue", "Brown", "Hazel", "Amber"], ensure_ascii=False),
"hint_en": "It is Daniel Radcliffe's natural eye color."
},
{
"category": "movies",
"difficulty": "hard",
"question_type": "text",
"question": "Welcher unvergessene britische Schauspieler verkörperte Severus Snape in allen acht Filmen?",
"canonical_answer": "Alan Rickman",
"accepted_answers": json.dumps(["Alan Rickman", "Rickman"]),
"hint": "J.K. Rowling verriet ihm schon vor dem Erscheinen des letzten Buches das Geheimnis hinter Snapes 'Always'.",
"question_en": "Which beloved British actor portrayed Severus Snape across all eight Harry Potter films?",
"answer_en": "Alan Rickman",
"hint_en": "J.K. Rowling privately told him the secret behind Snape's 'Always' years before the final book was published."
},
{
"category": "movies",
"difficulty": "hard",
"question_type": "multiple_choice",
"question": "An welchem historischen englischen Bahnhof wurden die Außenaufnahmen von Hogsmeade Station in den ersten Filmen gedreht?",
"canonical_answer": "Goathland Station (North Yorkshire)",
"options": json.dumps([
"Goathland Station (North Yorkshire)",
"Paddington Station (London)",
"Waverley Station (Edinburgh)",
"York Central Station"
], ensure_ascii=False),
"hint": "Eine malerische Museumsbahnstation der North Yorkshire Moors Railway.",
"question_en": "At which historic English heritage railway station were the Hogsmeade Station scenes filmed in the early movies?",
"answer_en": "Goathland Station (North Yorkshire)",
"options_en": json.dumps([
"Goathland Station (North Yorkshire)",
"Paddington Station (London)",
"Waverley Station (Edinburgh)",
"York Central Station"
], ensure_ascii=False),
"hint_en": "A picturesque station on the North Yorkshire Moors Railway."
},
{
"category": "movies",
"difficulty": "medium",
"question_type": "multiple_choice",
"question": "Auf welchem echten Londoner Bahnhof wurden die Szenen am Gleis 9¾ gedreht?",
"canonical_answer": "King's Cross",
"options": json.dumps(["King's Cross", "Waterloo", "Victoria Station", "Euston"], ensure_ascii=False),
"hint": "Die Außenfassade im 2. Film zeigt das benachbarte St Pancras Renaissance Hotel.",
"question_en": "At which real-life London railway station were the Platform 9¾ scenes filmed?",
"answer_en": "King's Cross",
"options_en": json.dumps(["King's Cross", "Waterloo", "Victoria Station", "Euston"], ensure_ascii=False),
"hint_en": "Today, fans can take photos with a luggage trolley disappearing into the brick wall there."
},
{
"category": "movies",
"difficulty": "hard",
"question_type": "multiple_choice",
"question": "Wie heißt die fiktive Zauberrockband im 4. Film beim Weihnachtsball, deren Sänger von Pulp-Frontmann Jarvis Cocker gespielt wurde?",
"canonical_answer": "The Weird Sisters (Schwestern des Schicksals)",
"options": json.dumps([
"The Weird Sisters (Schwestern des Schicksals)",
"Celestina Warbeck & the Banshees",
"The Hobgoblins",
"Frog Choir"
], ensure_ascii=False),
"hint": "Auch Mitglieder der Band Radiohead spielten in dieser Filmszene in der Band mit!",
"question_en": "What is the name of the wizarding rock band at the Yule Ball in the fourth movie, fronted by Pulp's Jarvis Cocker?",
"answer_en": "The Weird Sisters",
"options_en": json.dumps([
"The Weird Sisters",
"Celestina Warbeck & the Banshees",
"The Hobgoblins",
"The Frog Choir"
], ensure_ascii=False),
"hint_en": "Members of Radiohead also appeared in the band lineup for that scene!"
},
# --- SPELLS & POTIONS (Zaubersprüche & Tränke) ---
{
"category": "spells",
"difficulty": "hard",
"question_type": "text",
"question": "Wie lautet der Gegenzauber zum Schwebezauber 'Levicorpus', mit dem man das Opfer wieder auf den Boden holt?",
"canonical_answer": "Liberacorpus",
"accepted_answers": json.dumps(["Liberacorpus", "Libera corpus"]),
"hint": "Harry findet sowohl den Zauber als auch den Gegenzauber handschriftlich im Buch des Halbblutprinzen.",
"question_en": "What is the counter-jinx to 'Levicorpus' that lowers the victim back to the ground?",
"answer_en": "Liberacorpus",
"hint_en": "Harry discovers both the spell and its counter-jinx written in the margins of the Half-Blood Prince's textbook."
},
{
"category": "spells",
"difficulty": "hard",
"question_type": "multiple_choice",
"question": "Welcher seltene Stein aus dem Magen einer Ziege wirkt als universelles Gegengift gegen die meisten Zaubertrank-Gifte?",
"canonical_answer": "Bezoar",
"options": json.dumps(["Bezoar", "Mondstein", "Blutstein", "Flussstein"], ensure_ascii=False),
"hint": "Snape fragt Harry danach in seiner allerersten Zaubertränkestunde, und Harry rettet damit später Rons Leben.",
"question_en": "Which stone taken from the stomach of a goat acts as an antidote to most poisons?",
"answer_en": "Bezoar",
"options_en": json.dumps(["Bezoar", "Moonstone", "Bloodstone", "Runestone"], ensure_ascii=False),
"hint_en": "Snape quizzes Harry about it in his very first Potions lesson, and Harry later uses one to save Ron's life."
},
{
"category": "spells",
"difficulty": "medium",
"question_type": "text",
"question": "Welcher dunkle Fluch, den der Halbblutprinz erfunden hat, schlitzt das Opfer wie mit einem unsichtbaren Schwert auf ('Für Feinde')?",
"canonical_answer": "Sectumsempra",
"accepted_answers": json.dumps(["Sectumsempra", "Sectum Sempra"]),
"hint": "Harry wendet ihn im 6. Teil im Waschraum gegen Draco Malfoy an.",
"question_en": "Which dark curse invented by the Half-Blood Prince slashes the target as if with an invisible sword ('For Enemies')?",
"answer_en": "Sectumsempra",
"hint_en": "Harry uses it against Draco Malfoy in the bathroom in year six."
},
{
"category": "spells",
"difficulty": "medium",
"question_type": "multiple_choice",
"question": "Wie lange dauert die aufwendige Zubereitung des Vielsaft-Tranks (Polyjuice Potion) ungefähr?",
"canonical_answer": "Einen ganzen Monat",
"options": json.dumps(["Einen ganzen Monat", "Genau 24 Stunden", "Sieben Tage", "Nur eine Vollmondnacht"], ensure_ascii=False),
"hint": "Unter anderem muss Florfliegenkraut 21 Tage lang schmoren.",
"question_en": "Approximately how long does it take to brew Polyjuice Potion from start to finish?",
"answer_en": "One full month",
"options_en": json.dumps(["One full month", "Exactly 24 hours", "Seven days", "A single full-moon night"], ensure_ascii=False),
"hint_en": "Lacewing flies alone must stew for twenty-one days."
},
# --- HOGWARTS & PLACES ---
{
"category": "hogwarts",
"difficulty": "hard",
"question_type": "multiple_choice",
"question": "Wie gelangt man in die Schulküche von Hogwarts, die direkt unter der Großen Halle liegt?",
"canonical_answer": "Man kitzelt die Birne auf einem Obst-Gemälde",
"options": json.dumps([
"Man kitzelt die Birne auf einem Obst-Gemälde",
"Man klopft dreimal auf ein Essigfass",
"Man sagt das Passwort 'Kürbispastete' zu einer Rüstung",
"Man zieht an einer goldenen Bratpfanne"
], ensure_ascii=False),
"hint": "Die Frucht kichert daraufhin und verwandelt sich in einen grünen Türgriff.",
"question_en": "How do you open the secret entrance to the Hogwarts kitchens located beneath the Great Hall?",
"answer_en": "Tickle the pear in the painting of a bowl of fruit",
"options_en": json.dumps([
"Tickle the pear in the painting of a bowl of fruit",
"Tap a vinegar barrel in the rhythm of Helga Hufflepuff",
"Whisper 'Pumpkin Pasties' to a suit of armor",
"Pull on a golden ladle"
], ensure_ascii=False),
"hint_en": "The fruit giggles and transforms into a green door handle."
},
{
"category": "hogwarts",
"difficulty": "hard",
"question_type": "multiple_choice",
"question": "Was unterscheidet den Eingang zum Ravenclaw-Gemeinschaftsraum von denen der anderen drei Häuser?",
"canonical_answer": "Man muss eine philosophische Rätselfrage des Türklopfers beantworten statt ein Passwort zu nennen",
"options": json.dumps([
"Man muss eine philosophische Rätselfrage des Türklopfers beantworten statt ein Passwort zu nennen",
"Man kann nur auf einem Besen durch das Turmfenster hineinfliegen",
"Man muss seinen Zauberstab auf eine Waage legen",
"Die Tür öffnet sich nur bei Mondschein"
], ensure_ascii=False),
"hint": "Der bronzene Adler-Türklopfer fragt z. B.: 'Was war zuerst da, der Phönix oder die Flamme?'",
"question_en": "How does entering the Ravenclaw common room differ from the other three Hogwarts houses?",
"answer_en": "You must answer a philosophical riddle posed by a bronze eagle knocker instead of giving a password",
"options_en": json.dumps([
"You must answer a philosophical riddle posed by a bronze eagle knocker instead of giving a password",
"You can only enter by flying through the tower window on a broom",
"You must weigh your wand on an enchanted scale",
"The door only opens under moonlight"
], ensure_ascii=False),
"hint_en": "For example, the bronze eagle asks: 'Which came first, the phoenix or the flame?'"
},
{
"category": "hogwarts",
"difficulty": "medium",
"question_type": "text",
"question": "Unter welchem Baum auf dem Schulgelände von Hogwarts beginnt der geheime Tunnel, der direkt in die Heulende Hütte nach Hogsmeade führt?",
"canonical_answer": "Peitschende Weide",
"accepted_answers": json.dumps(["Peitschende Weide", "Die Peitschende Weide", "Whomping Willow"]),
"hint": "Er wurde gepflanzt, als Remus Lupin als Schüler nach Hogwarts kam.",
"question_en": "Beneath which violent tree on the Hogwarts grounds is the secret tunnel leading to the Shrieking Shack?",
"answer_en": "Whomping Willow",
"hint_en": "It was planted the year Remus Lupin arrived at Hogwarts as a student."
},
{
"category": "hogwarts",
"difficulty": "easy",
"question_type": "text",
"question": "Wie heißt das einzige komplett von Zauberern bewohnte Dorf in Großbritannien, das in der Nähe von Hogwarts liegt?",
"canonical_answer": "Hogsmeade",
"accepted_answers": json.dumps(["Hogsmeade"]),
"hint": "Schüler ab der dritten Klasse dürfen es an bestimmten Wochenenden mit einer Unterschrift besuchen.",
"question_en": "What is the name of the only all-wizarding village in Britain, located near Hogwarts?",
"answer_en": "Hogsmeade",
"hint_en": "Students in their third year and above may visit it on certain weekends with a signed permission slip."
},
# --- QUIDDITCH ---
{
"category": "quidditch",
"difficulty": "hard",
"question_type": "multiple_choice",
"question": "Welches spektakuläre Flugmanöver führt Viktor Krum beim Finale der Quidditch-Weltmeisterschaft 1994 aus, um den gegnerischen Sucher Aidan Lynch in den Boden stürzen zu lassen?",
"canonical_answer": "Wronski-Bluff (Wronski Feint)",
"options": json.dumps([
"Wronski-Bluff (Wronski Feint)",
"Porskoff-Ploy",
"Falkenkopf-Angriffsformation",
"Faultier-Rolle (Sloth Grip Roll)"
], ensure_ascii=False),
"hint": "Der Sucher täuscht im Sturzflug vor, den Goldenen Schnatz am Boden gesehen zu haben, und zieht im letzten Moment hoch.",
"question_en": "Which famous diving maneuver does Viktor Krum execute during the 1994 Quidditch World Cup Final to trick Irish Seeker Aidan Lynch?",
"answer_en": "Wronski Feint",
"options_en": json.dumps([
"Wronski Feint",
"Porskoff Ploy",
"Hawkshead Attacking Formation",
"Sloth Grip Roll"
], ensure_ascii=False),
"hint_en": "The Seeker dives steeply toward the ground pretending to have spotted the Snitch, pulling up at the very last second."
},
{
"category": "quidditch",
"difficulty": "hard",
"question_type": "multiple_choice",
"question": "Welches magische Wesen war vor der Erfindung des Goldenen Schnatzes das ursprüngliche Jagdziel beim Quidditch?",
"canonical_answer": "Der Goldene Schnatzer (Golden Snidget)",
"options": json.dumps([
"Der Goldene Schnatzer (Golden Snidget)",
"Der Fwuuper",
"Der Augurey",
"Der Jobberknoll"
], ensure_ascii=False),
"hint": "Ein kugelrunder kleiner goldener Vogel, der dadurch fast ausgerottet wurde, bevor Bowman Wright den metallenen Schnatz schmiedete.",
"question_en": "Before the invention of the metallic Golden Snitch, which small magical bird was hunted during Quidditch matches?",
"answer_en": "Golden Snidget",
"options_en": json.dumps([
"Golden Snidget",
"Fwooper",
"Augurey",
"Jobberknoll"
], ensure_ascii=False),
"hint_en": "The tiny spherical golden bird was nearly hunted to extinction before Bowman Wright forged the enchanted metal replacement."
},
{
"category": "quidditch",
"difficulty": "medium",
"question_type": "multiple_choice",
"question": "Welche beiden Nationalmannschaften standen sich im Finale der Quidditch-Weltmeisterschaft 1994 gegenüber?",
"canonical_answer": "Irland und Bulgarien",
"options": json.dumps([
"Irland und Bulgarien",
"England und Transsilvanien",
"Schottland und Frankreich",
"Irland und Rumänien"
], ensure_ascii=False),
"hint": "Viktor Krum fing zwar den Schnatz, doch die andere Mannschaft gewann trotzdem mit 170 zu 160 Punkten!",
"question_en": "Which two national teams faced off in the final of the 1994 Quidditch World Cup?",
"answer_en": "Ireland and Bulgaria",
"options_en": json.dumps([
"Ireland and Bulgaria",
"England and Transylvania",
"Scotland and France",
"Ireland and Romania"
], ensure_ascii=False),
"hint_en": "Viktor Krum caught the Snitch, yet the opposing team still won the match 170 to 160!"
},
{
"category": "quidditch",
"difficulty": "easy",
"question_type": "text",
"question": "Wie heißt das schnellste Rennbesen-Modell der Welt, das Harry im dritten Schuljahr anonym von seinem Paten Sirius Black geschenkt bekommt?",
"canonical_answer": "Feuerblitz",
"accepted_answers": json.dumps(["Feuerblitz", "Der Feuerblitz", "Firebolt"]),
"hint": "Er löst Harrys zerstörten Nimbus 2000 ab.",
"question_en": "What is the name of the world-class racing broom Harry receives anonymously from his godfather Sirius Black in his third year?",
"answer_en": "Firebolt",
"hint_en": "It replaces Harry's destroyed Nimbus 2000."
},
# --- CHARACTERS ---
{
"category": "characters",
"difficulty": "hard",
"question_type": "text",
"question": "In welches Tier verwandelt sich Rita Kimmkorn (Rita Skeeter) als nicht registrierter Animagus, um unbemerkt Gespräche zu belauschen?",
"canonical_answer": "Käfer",
"accepted_answers": json.dumps(["Käfer", "Ein Käfer", "Wasserkäfer", "Grüner Käfer", "Beetle"]),
"hint": "Hermine fängt sie am Ende des 4. Schuljahres in einem unzerbrechlichen Schraubglas.",
"question_en": "Into which insect does journalist Rita Skeeter transform as an unregistered Animagus to eavesdrop on conversations?",
"answer_en": "Beetle",
"hint_en": "Hermione traps her inside an unbreakable glass jar at the end of year four."
},
{
"category": "characters",
"difficulty": "hard",
"question_type": "multiple_choice",
"question": "Wer hinterließ die geheimnisvolle Notiz mit den Initialen 'R.A.B.' in der falschen Horkrux-Höhle?",
"canonical_answer": "Regulus Arcturus Black",
"options": json.dumps([
"Regulus Arcturus Black",
"Rodolphus Augustus Bell",
"Rufus Albus Bones",
"Rabastan Lestrange"
], ensure_ascii=False),
"hint": "Der jüngere Bruder von Sirius Black, der mit Hilfe des Hauselfen Kreacher das echte Medaillon stahl.",
"question_en": "Who left the mysterious note signed 'R.A.B.' inside the fake Horcrux locket in the sea cave?",
"answer_en": "Regulus Arcturus Black",
"options_en": json.dumps([
"Regulus Arcturus Black",
"Rodolphus Augustus Bell",
"Rufus Albus Bones",
"Rabastan Lestrange"
], ensure_ascii=False),
"hint_en": "Sirius Black's younger brother, who stole the real locket with the help of the house-elf Kreacher."
}
]
def seed_lore_pack():
db = SessionLocal()
try:
existing_questions = {q.question.strip().lower(): q for q in db.query(Question).all()}
max_sort = db.query(Question.sort_order).order_by(Question.sort_order.desc()).first()
next_sort = (max_sort[0] + 1) if max_sort and max_sort[0] else 100
added = 0
for item in LORE_PACK_QUESTIONS:
q_key = item["question"].strip().lower()
if q_key in existing_questions:
continue
new_q = Question(
category=item["category"],
difficulty=item["difficulty"],
question_type=item["question_type"],
question=item["question"],
canonical_answer=item["canonical_answer"],
accepted_answers=item.get("accepted_answers"),
options=item.get("options"),
hint=item.get("hint"),
question_en=item.get("question_en"),
answer_en=item.get("answer_en"),
options_en=item.get("options_en"),
hint_en=item.get("hint_en"),
status="active",
sort_order=next_sort,
submitted_by="System Lore Pack"
)
db.add(new_q)
next_sort += 1
added += 1
db.commit()
print(f"Lore Pack Seeding complete: {added} new questions added. Total active: {db.query(Question).filter(Question.status == 'active').count()}")
finally:
db.close()
if __name__ == "__main__":
seed_lore_pack()
+1465 -1765
View File
File diff suppressed because it is too large Load Diff
+606 -586
View File
File diff suppressed because it is too large Load Diff
+25 -17
View File
@@ -1,18 +1,20 @@
const SUGGESTION_TEXT = {
de: {
title: "✨ Neue Frage für das Quiz vorschlagen ✨",
subtitle: "Bereichere das magische Quiz mit kniffligen Harry-Potter- oder Escape-Rätseln!",
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.",
nameLabel: "Dein Name / Zauberer-Alias (optional):",
title: "✍️ Neue Quiz-Frage einreichen",
subtitle: "Bereichere PotterTrivia mit deinen eigenen Fragen zu Büchern, Filmen oder magischen Geschöpfen!",
alert: "ℹ️ <strong>Hinweis zur Moderation:</strong> Eingereichte Fragen werden kurz auf Richtigkeit geprüft, bevor sie im öffentlichen Quiz-Pool freigeschaltet werden.",
nameLabel: "Dein Name / Alias (optional):",
namePlaceholder: "z. B. Hermine Granger",
catLabel: "Kategorie / Themenbereich:",
catHogwarts: "🏰 Hogwarts & Schulgelände",
catCharacters: "🧙 Charaktere & Personen",
catSpells: "⚡ Zaubersprüche & Magie",
catCreatures: "🐉 Magische Tierwesen",
catBooks: "📖 Buchwissen (Buch-Details)",
catMovies: "🎬 Filmwissen (Kino & Drehorte)",
catHogwarts: "🏰 Hogwarts & Orte",
catCharacters: "🧙‍♂️ Charaktere & Personen",
catSpells: "🪄 Zaubersprüche & Tränke",
catCreatures: "🐉 Magische Geschöpfe",
catQuidditch: "🧹 Quidditch & Sport",
catLore: "🔮 Artefakte & Geschichte",
catGeneral: "📜 Allgemeines Zauberwissen",
catGeneral: "⚡ Allgemeines Wissen",
frageLabel: "Deine Frage: *",
fragePlaceholder: "z. B. Wie heißt der Schulleiter von Hogwarts?",
diffLabel: "Schwierigkeitsgrad:",
@@ -36,19 +38,21 @@ const SUGGESTION_TEXT = {
successHeader: "✨ Vorschlag erfolgreich übermittelt! ✨"
},
en: {
title: "✨ Suggest a New Quiz Question ✨",
subtitle: "Enrich the magical quiz with tricky Harry Potter or escape puzzles!",
alert: "ℹ️ <strong>Moderation Note:</strong> Submitted questions are not immediately public; they automatically receive the status <em>'Pending Review'</em>. The Ministry of Magic tests and verifies the answer before approval.",
nameLabel: "Your Name / Wizard Alias (optional):",
title: "✍️ Submit a New Quiz Question",
subtitle: "Enrich PotterTrivia with your own questions about the books, movies, or magical creatures!",
alert: "ℹ️ <strong>Moderation Note:</strong> Submitted questions are briefly reviewed for accuracy before going live in the public quiz pool.",
nameLabel: "Your Name / Alias (optional):",
namePlaceholder: "e.g. Hermione Granger",
catLabel: "Category / Topic Area:",
catHogwarts: "🏰 Hogwarts & School Grounds",
catCharacters: "🧙 Characters & People",
catSpells: "⚡ Spells & Magic",
catBooks: "📖 Book Lore (Book Details)",
catMovies: "🎬 Movie Trivia (Cinema & Filming)",
catHogwarts: "🏰 Hogwarts & Places",
catCharacters: "🧙‍♂️ Characters & People",
catSpells: "🪄 Spells & Potions",
catCreatures: "🐉 Magical Creatures",
catQuidditch: "🧹 Quidditch & Sport",
catLore: "🔮 Artifacts & Lore",
catGeneral: "📜 General Wizarding Lore",
catGeneral: "⚡ General Knowledge",
frageLabel: "Your Question: *",
fragePlaceholder: "e.g. Who is the Headmaster of Hogwarts?",
diffLabel: "Difficulty Level:",
@@ -92,6 +96,8 @@ function onLanguageChange(lang) {
const submitBtn = document.getElementById("submitBtn");
const catLabel = document.getElementById("suggCatLabel");
const catBooks = document.getElementById("catBooks");
const catMovies = document.getElementById("catMovies");
const catHogwarts = document.getElementById("catHogwarts");
const catCharacters = document.getElementById("catCharacters");
const catSpells = document.getElementById("catSpells");
@@ -106,6 +112,8 @@ function onLanguageChange(lang) {
if (nameLabel) nameLabel.innerText = t.nameLabel;
if (nameInput) nameInput.placeholder = t.namePlaceholder;
if (catLabel) catLabel.innerText = t.catLabel;
if (catBooks) catBooks.innerText = t.catBooks;
if (catMovies) catMovies.innerText = t.catMovies;
if (catHogwarts) catHogwarts.innerText = t.catHogwarts;
if (catCharacters) catCharacters.innerText = t.catCharacters;
if (catSpells) catSpells.innerText = t.catSpells;
+9 -3
View File
@@ -225,9 +225,15 @@
<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>
<option value="books">📖 Buchwissen</option>
<option value="movies">🎬 Filmwissen</option>
<option value="creatures">🐉 Magische Geschöpfe</option>
<option value="spells">🪄 Zaubersprüche & Tränke</option>
<option value="characters" selected>🧙‍♂️ Charaktere</option>
<option value="hogwarts">🏰 Hogwarts & Orte</option>
<option value="quidditch">🧹 Quidditch</option>
<option value="lore">🔮 Artefakte & Lore</option>
<option value="general">⚡ Allgemein</option>
</select>
</div>
+117 -165
View File
@@ -1,165 +1,117 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0">
<title>{% block title %}Magisches Harry-Potter-Escape-Quiz{% endblock %}</title>
<link rel="icon" href="/static/favicon.ico">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Caveat:wght@500;700&family=Cinzel:wght@600;700&family=Marck+Script&family=MedievalSharp&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/css/style.css?v=3.2">
{% block head %}{% endblock %}
</head>
<body>
<audio id="bgm" src="/static/audio/music.mp3" loop preload="auto"></audio>
<header class="site-header">
<div class="header-inner">
<a href="/" class="brand-link">
<span class="brand-sparkle">⚡</span>
<span class="brand-title">Hogwarts Quiz</span>
</a>
<div class="header-right">
<div class="lang-switch-group" role="group" aria-label="Sprachauswahl / Language">
<button type="button" id="langDe" class="lang-btn active" onclick="setLanguage('de')" title="Deutsch">🇩🇪 DE</button>
<button type="button" id="langEn" class="lang-btn" onclick="setLanguage('en')" title="English">🇬🇧 EN</button>
</div>
<button id="audioToggle" class="audio-btn" title="Musik an/aus" onclick="toggleAudio()">
<span id="audioIcon">🔇</span>
<span id="audioText" class="audio-label">Musik: Aus</span>
</button>
<button id="menuToggle" class="menu-toggle-btn" aria-label="Menü öffnen" onclick="toggleMobileMenu()">
<span id="menuIcon">☰</span>
</button>
</div>
<nav id="navLinks" class="nav-links">
<a href="/" class="nav-link" id="navQuiz" onclick="closeMobileMenu()">✨ Zum Quiz</a>
<a href="/bestenliste" class="nav-link" id="navLeaderboard" onclick="closeMobileMenu()">🏆 Bestenliste</a>
<a href="/vorschlag" class="nav-link" id="navSuggest" onclick="closeMobileMenu()">✍️ Frage einreichen</a>
<a href="/admin" class="nav-link" id="navAdmin" onclick="closeMobileMenu()">🔒 Admin-Cockpit</a>
</nav>
</div>
</header>
<main class="container">
{% block content %}{% endblock %}
</main>
<footer class="footer">
<p id="footerText">⚡ Magisches Hogwarts-Escape-Quiz · Betrieben mit Liebe & Zauberkraft ⚡</p>
<div class="footer-links">
<a href="/impressum" class="footer-link" id="footerImpressum">⚖️ Impressum</a>
<span class="footer-sep">·</span>
<a href="/datenschutz" class="footer-link" id="footerDatenschutz">🛡️ Datenschutz</a>
</div>
</footer>
<script>
const bgm = document.getElementById("bgm");
const audioIcon = document.getElementById("audioIcon");
const audioText = document.getElementById("audioText");
const navLinks = document.getElementById("navLinks");
const menuIcon = document.getElementById("menuIcon");
let currentLang = localStorage.getItem("quiz_lang") || "de";
function setLanguage(lang) {
currentLang = (lang === "en") ? "en" : "de";
localStorage.setItem("quiz_lang", currentLang);
document.documentElement.lang = currentLang;
updateLanguageUI();
if (typeof onLanguageChange === "function") {
onLanguageChange(currentLang);
}
}
function updateLanguageUI() {
const isDe = currentLang === "de";
const deBtn = document.getElementById("langDe");
const enBtn = document.getElementById("langEn");
if (deBtn && enBtn) {
deBtn.classList.toggle("active", isDe);
enBtn.classList.toggle("active", !isDe);
}
const navQuiz = document.getElementById("navQuiz");
const navLeaderboard = document.getElementById("navLeaderboard");
const navSuggest = document.getElementById("navSuggest");
const navAdmin = document.getElementById("navAdmin");
const footerText = document.getElementById("footerText");
const footerImpressum = document.getElementById("footerImpressum");
const footerDatenschutz = document.getElementById("footerDatenschutz");
if (navQuiz) navQuiz.innerText = isDe ? "✨ Zum Quiz" : "✨ To Quiz";
if (navLeaderboard) navLeaderboard.innerText = isDe ? "🏆 Bestenliste" : "🏆 Leaderboard";
if (navSuggest) navSuggest.innerText = isDe ? "✍️ Frage einreichen" : "✍️ Submit Question";
if (navAdmin) navAdmin.innerText = isDe ? "🔒 Admin-Cockpit" : "🔒 Admin Cockpit";
if (footerText) {
footerText.innerText = isDe
? "⚡ Magisches Hogwarts-Escape-Quiz · Betrieben mit Liebe & Zauberkraft ⚡"
: "⚡ Magical Hogwarts Escape Quiz · Powered by Love & Wizardry ⚡";
}
if (footerImpressum) footerImpressum.innerText = isDe ? "⚖️ Impressum" : "⚖️ Legal Notice";
if (footerDatenschutz) footerDatenschutz.innerText = isDe ? "🛡️ Datenschutz" : "🛡️ Privacy Policy";
if (audioText) {
const isPlaying = bgm && !bgm.paused;
if (isDe) {
audioText.innerText = isPlaying ? "Musik: An" : "Musik: Aus";
} else {
audioText.innerText = isPlaying ? "Music: On" : "Music: Off";
}
}
}
function toggleAudio() {
const isDe = currentLang === "de";
if (bgm.paused) {
bgm.play().then(() => {
audioIcon.innerText = "🔊";
audioText.innerText = isDe ? "Musik: An" : "Music: On";
localStorage.setItem("bgm_enabled", "true");
}).catch(err => {
console.log("Audio play blocked:", err);
});
} else {
bgm.pause();
audioIcon.innerText = "🔇";
audioText.innerText = isDe ? "Musik: Aus" : "Music: Off";
localStorage.setItem("bgm_enabled", "false");
}
}
function toggleMobileMenu() {
const isOpen = navLinks.classList.toggle("nav-open");
menuIcon.innerText = isOpen ? "✕" : "☰";
}
function closeMobileMenu() {
navLinks.classList.remove("nav-open");
menuIcon.innerText = "☰";
}
// 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 = (currentLang === "de") ? "Musik: An" : "Music: On";
}).catch(() => {});
}
document.removeEventListener("click", initAudioOnInteraction);
}, { once: true });
// Initialize UI on load
document.addEventListener("DOMContentLoaded", () => {
updateLanguageUI();
});
</script>
{% block scripts %}{% endblock %}
</body>
</html>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0">
<title>{% block title %}PotterTrivia – Das Harry-Potter-Wissensquiz{% endblock %}</title>
<link rel="icon" href="/static/favicon.ico">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Space+Grotesk:wght@600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/css/style.css?v=4.0">
{% block head %}{% endblock %}
</head>
<body>
<header class="site-header">
<div class="header-inner">
<a href="/" class="brand-link">
<span class="brand-badge">⚡</span>
<span class="brand-title">Potter<span class="brand-accent">Trivia</span></span>
</a>
<nav id="navLinks" class="nav-links">
<a href="/" class="nav-link" id="navQuiz" onclick="closeMobileMenu()">🎯 Quiz</a>
<a href="/bestenliste" class="nav-link" id="navLeaderboard" onclick="closeMobileMenu()">🏆 Bestenliste</a>
<a href="/vorschlag" class="nav-link" id="navSuggest" onclick="closeMobileMenu()">✍️ Frage einreichen</a>
<a href="/admin" class="nav-link" id="navAdmin" onclick="closeMobileMenu()">⚙️ Admin</a>
</nav>
<div class="header-right">
<div class="lang-switch-group" role="group" aria-label="Sprachauswahl / Language">
<button type="button" id="langDe" class="lang-btn active" onclick="setLanguage('de')" title="Deutsch">🇩🇪 DE</button>
<button type="button" id="langEn" class="lang-btn" onclick="setLanguage('en')" title="English">🇬🇧 EN</button>
</div>
<button id="menuToggle" class="menu-toggle-btn" aria-label="Menü öffnen" onclick="toggleMobileMenu()">
<span id="menuIcon">☰</span>
</button>
</div>
</div>
</header>
<main class="container">
{% block content %}{% endblock %}
</main>
<footer class="footer">
<p id="footerText">⚡ PotterTrivia · Das unabhängige Harry-Potter-Wissensquiz</p>
<div class="footer-links">
<a href="/impressum" class="footer-link" id="footerImpressum">⚖️ Impressum</a>
<span class="footer-sep">·</span>
<a href="/datenschutz" class="footer-link" id="footerDatenschutz">🛡️ Datenschutz</a>
</div>
</footer>
<script>
const navLinks = document.getElementById("navLinks");
const menuIcon = document.getElementById("menuIcon");
let currentLang = localStorage.getItem("quiz_lang") || "de";
function setLanguage(lang) {
currentLang = (lang === "en") ? "en" : "de";
localStorage.setItem("quiz_lang", currentLang);
document.documentElement.lang = currentLang;
updateLanguageUI();
if (typeof onLanguageChange === "function") {
onLanguageChange(currentLang);
}
}
function updateLanguageUI() {
const isDe = currentLang === "de";
const deBtn = document.getElementById("langDe");
const enBtn = document.getElementById("langEn");
if (deBtn && enBtn) {
deBtn.classList.toggle("active", isDe);
enBtn.classList.toggle("active", !isDe);
}
const navQuiz = document.getElementById("navQuiz");
const navLeaderboard = document.getElementById("navLeaderboard");
const navSuggest = document.getElementById("navSuggest");
const navAdmin = document.getElementById("navAdmin");
const footerText = document.getElementById("footerText");
const footerImpressum = document.getElementById("footerImpressum");
const footerDatenschutz = document.getElementById("footerDatenschutz");
if (navQuiz) navQuiz.innerText = isDe ? "🎯 Quiz" : "🎯 Quiz";
if (navLeaderboard) navLeaderboard.innerText = isDe ? "🏆 Bestenliste" : "🏆 Leaderboard";
if (navSuggest) navSuggest.innerText = isDe ? "✍️ Frage einreichen" : "✍️ Submit Question";
if (navAdmin) navAdmin.innerText = isDe ? "⚙️ Admin" : "⚙️ Admin";
if (footerText) {
footerText.innerText = isDe
? "⚡ PotterTrivia · Das unabhängige Harry-Potter-Wissensquiz"
: "⚡ PotterTrivia · The Independent Wizarding World Trivia Quiz";
}
if (footerImpressum) footerImpressum.innerText = isDe ? "⚖️ Impressum" : "⚖️ Legal Notice";
if (footerDatenschutz) footerDatenschutz.innerText = isDe ? "🛡️ Datenschutz" : "🛡️ Privacy Policy";
}
function toggleMobileMenu() {
const isOpen = navLinks.classList.toggle("nav-open");
menuIcon.innerText = isOpen ? "✕" : "☰";
}
function closeMobileMenu() {
navLinks.classList.remove("nav-open");
menuIcon.innerText = "☰";
}
document.addEventListener("DOMContentLoaded", () => {
updateLanguageUI();
});
</script>
{% block scripts %}{% endblock %}
</body>
</html>
+160 -174
View File
@@ -1,174 +1,160 @@
{% 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" id="pageTitle">✨ Magisches Hogwarts-Escape-Quiz ✨</h1>
<p class="subtitle" id="pageSubtitle">10 knifflige Rätsel aus der Zauberwelt warten auf dich</p>
</header>
<!-- Brief-Intro / Startmaske -->
<div id="introCard" class="card intro-card intro-parchment-wrapper">
<!-- Antiker geschlossener Hogwarts-Umschlag (öffnet sich bei Klick auf das Siegel) -->
<div class="antique-envelope-scene" id="envelope">
<div class="envelope-base">
<!-- Dunkleres Pergament-Innenfutter -->
<div class="envelope-interior"></div>
<!-- Der darin liegende Briefbogen (gleitet beim Öffnen majestätisch hervor) -->
<div class="letter-sheet" id="letterSheet">
<div class="sheet-parchment-layer"></div>
<header class="letter-header">
<div class="letter-crest-box">
<img src="/static/img/hogwarts_seal.jpg" alt="Hogwarts Crest" class="letter-crest-sketch">
</div>
<h3 class="letter-school-name" id="letterSchoolTitle">Hogwarts-Schule für Hexerei und Zauberei</h3>
<p class="letter-headmaster" id="letterHeadmaster">Schulleiter: Albus Dumbledore<br><span>(Großhexenmeister, Träger des Merlin-Ordens 1. Klasse)</span></p>
<div class="letter-ornament-line">
<span class="ornament-leaf">❦</span>
<span class="ornament-rule"></span>
<span class="ornament-shield">✦</span>
<span class="ornament-rule"></span>
<span class="ornament-leaf">❧</span>
</div>
</header>
<div class="letter-body-handwritten">
<p class="letter-salutation" id="letterSalutation">Liebe Hexe, lieber Zauberer,</p>
<p class="letter-paragraph" id="letterParagraph1">
wir freuen uns, dir mitteilen zu dürfen, dass du zu den <strong>Magischen Z.A.G.-Prüfungen</strong> an der Hogwarts-Schule zugelassen bist.
</p>
<p class="letter-paragraph" id="letterParagraph2">
Zehn anspruchsvolle Prüfungsfragen entscheiden über deinen Rang in den Chroniken. Zeige deinen Scharfsinn und sichere bis zu <strong>1.000 Hauspunkte</strong> für dein Haus!
</p>
</div>
<footer class="letter-signature-block">
<p class="letter-closing" id="letterClosing">Mit vorzüglicher Hochachtung,</p>
<p class="letter-signature-ink" id="letterSignature">Minerva McGonagall</p>
<p class="letter-sign-title" id="letterSignTitle">Stellvertretende Schulleiterin</p>
</footer>
</div>
<!-- Vordere Umschlagtasche mit handschriftlicher Smaragd-Anschrift -->
<div class="envelope-front-pocket">
<div class="envelope-sketch-border"></div>
<div class="envelope-address-block">
<p class="addr-line addr-school" id="envelopeLetter">Hogwarts-Schule für Hexerei und Zauberei</p>
<p class="addr-line addr-recipient" id="envelopeRecipient">An den ehrenwerten Zauberschüler</p>
<p class="addr-line addr-location" id="envelopeLocation">Auf dem Prüfungspfad • Schloss Hogwarts</p>
</div>
</div>
<!-- Obere Klappe des Umschlags (überdeckt die Tasche und wird vom Siegel gehalten) -->
<div class="envelope-closing-flap">
<div class="flap-sketch-line"></div>
</div>
<!-- Handgezeichnetes Wachssiegel über der Klappenspitze -->
<div class="sketched-wax-seal" id="waxSeal" onclick="startGame()" title="Siegel brechen & Brief öffnen">
<img src="/static/img/hogwarts_seal.jpg" alt="Hogwarts-Wappen Siegel" class="seal-crest-img">
<div class="seal-emboss-ring"></div>
<div class="seal-glow"></div>
</div>
</div>
</div>
<!-- Auswahlbereich unter dem Pergament -->
<div class="intro-content intro-controls-panel">
<p class="intro-subnote" id="introDesc">
Ein verzauberter Pergamentbrief aus Hogwarts liegt vor dir.<br>
Wähle deinen Schwierigkeitsgrad und brich das rote Zaubersiegel:
</p>
<!-- Difficulty Selector Chips -->
<div class="difficulty-selection">
<span class="difficulty-label" id="diffLabel">Schwierigkeitsgrad wählen:</span>
<div class="difficulty-chips" role="radiogroup" aria-label="Schwierigkeitsgrad">
<button type="button" class="diff-chip active" data-diff="all" onclick="selectDifficulty('all')">
<span class="diff-icon">🎲</span> <span class="diff-text" id="diffAllText">Alle gemischt</span>
</button>
<button type="button" class="diff-chip" data-diff="easy" onclick="selectDifficulty('easy')">
<span class="diff-icon">🟢</span> <span class="diff-text" id="diffEasyText">Leicht</span>
</button>
<button type="button" class="diff-chip" data-diff="medium" onclick="selectDifficulty('medium')">
<span class="diff-icon">🟡</span> <span class="diff-text" id="diffMedText">Mittel</span>
</button>
<button type="button" class="diff-chip" data-diff="hard" onclick="selectDifficulty('hard')">
<span class="diff-icon">🔴</span> <span class="diff-text" id="diffHardText">Schwer</span>
</button>
</div>
</div>
<button id="startBtn" class="btn btn-primary btn-large btn-break-seal" onclick="startGame()">
📜 Siegel brechen & Brief öffnen
</button>
</div>
</div>
<!-- Spielbereich (initial versteckt) -->
<div id="gameCard" class="game-area hidden">
<!-- Fortschritt & Punkte & Zeit -->
<div class="status-panel">
<div class="progress-info">
<span id="questionCounter">Frage 1 von 10</span>
<span id="timerBadge" class="timer-badge">⏱️ 00:00</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">
<div class="meta-left">
<span id="badgeCategory" class="badge">⚡ Harry Potter</span>
<span id="badgeDifficulty" class="badge badge-diff">🟢 Leicht</span>
</div>
<span id="attemptBadge" class="attempt-badge">Versuch 1 von 3</span>
</div>
<h2 id="frageText" class="question-text">Lade verzauberte Schriftrollen...</h2>
<!-- Freitext-Eingabe (wenn question_type == 'text') -->
<div id="textAnswerGroup" 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()">
✨ Antwort prüfen
</button>
<button id="hintBtn" class="btn btn-secondary" onclick="requestHint()">
💡 Hinweis anzeigen
</button>
</div>
</div>
<!-- Multiple-Choice-Auswahl (wenn question_type == 'multiple_choice') -->
<div id="choiceAnswerGroup" class="choice-group hidden">
<div id="choiceOptionsContainer" class="choice-grid"></div>
<div class="btn-group" style="margin-top: 15px;">
<button id="hintBtnMC" class="btn btn-secondary" onclick="requestHint()">
💡 Hinweis anzeigen
</button>
</div>
</div>
<div id="hintBox" class="hint-box hidden">
<strong id="hintHeading">📜 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?v=3.2"></script>
{% endblock %}
{% extends "base.html" %}
{% block title %}⚡ PotterTrivia – Das Harry-Potter-Wissensquiz{% endblock %}
{% block content %}
<div class="quiz-wrapper">
<header class="quiz-header">
<div class="hero-pill" id="heroPill">⚡ {{ question_count }} Fragen im Wissens-Pool · Buch & Film</div>
<h1 class="glow-title" id="pageTitle">Harry Potter Wissensquiz</h1>
<p class="subtitle" id="pageSubtitle">Teste dein Wissen über Bücher, Filme, Geschöpfe und Zaubersprüche – direkt & ohne Umwege.</p>
</header>
<!-- Kompaktes Direct-Start Dashboard -->
<div id="introCard" class="card start-dashboard-card">
<div class="dashboard-grid">
<!-- Sektion 1: Schwierigkeitsgrad -->
<div class="config-section">
<div class="config-header">
<span class="config-step">1</span>
<span class="difficulty-label" id="diffLabel">Schwierigkeitsgrad</span>
</div>
<div class="difficulty-chips" role="radiogroup" aria-label="Schwierigkeitsgrad">
<button type="button" class="diff-chip active" data-diff="all" onclick="selectDifficulty('all')">
<span class="diff-icon">🎲</span>
<span class="diff-text" id="diffAllText">Alle Stufen</span>
</button>
<button type="button" class="diff-chip" data-diff="easy" onclick="selectDifficulty('easy')">
<span class="diff-icon">🟢</span>
<span class="diff-text" id="diffEasyText">Leicht</span>
</button>
<button type="button" class="diff-chip" data-diff="medium" onclick="selectDifficulty('medium')">
<span class="diff-icon">🟡</span>
<span class="diff-text" id="diffMedText">Mittel</span>
</button>
<button type="button" class="diff-chip" data-diff="hard" onclick="selectDifficulty('hard')">
<span class="diff-icon">🔴</span>
<span class="diff-text" id="diffHardText">Schwer (Experte)</span>
</button>
</div>
</div>
<!-- Sektion 2: Kategorie-Filter -->
<div class="config-section">
<div class="config-header">
<span class="config-step">2</span>
<span class="difficulty-label" id="catLabel">Themengebiet wählen</span>
</div>
<div class="category-chips" role="radiogroup" aria-label="Themengebiet">
<button type="button" class="cat-chip active" data-cat="all" onclick="selectCategory('all')">
<span>✨</span> <span id="catAllText">Alle Kategorien</span>
</button>
<button type="button" class="cat-chip" data-cat="books" onclick="selectCategory('books')">
<span>📖</span> <span id="catBooksText">Buchwissen</span>
</button>
<button type="button" class="cat-chip" data-cat="movies" onclick="selectCategory('movies')">
<span>🎬</span> <span id="catMoviesText">Filmwissen</span>
</button>
<button type="button" class="cat-chip" data-cat="creatures" onclick="selectCategory('creatures')">
<span>🐉</span> <span id="catCreaturesText">Magische Geschöpfe</span>
</button>
<button type="button" class="cat-chip" data-cat="spells" onclick="selectCategory('spells')">
<span>🪄</span> <span id="catSpellsText">Zaubersprüche & Tränke</span>
</button>
<button type="button" class="cat-chip" data-cat="characters" onclick="selectCategory('characters')">
<span>🧙‍♂️</span> <span id="catCharsText">Charaktere</span>
</button>
<button type="button" class="cat-chip" data-cat="hogwarts" onclick="selectCategory('hogwarts')">
<span>🏰</span> <span id="catHogwartsText">Hogwarts & Orte</span>
</button>
<button type="button" class="cat-chip" data-cat="quidditch" onclick="selectCategory('quidditch')">
<span>🧹</span> <span id="catQuidditchText">Quidditch</span>
</button>
<button type="button" class="cat-chip" data-cat="lore" onclick="selectCategory('lore')">
<span>🔮</span> <span id="catLoreText">Artefakte & Lore</span>
</button>
</div>
</div>
</div>
<div class="dashboard-footer">
<div class="dashboard-rules" id="dashboardRules">
<span>🎯 10 Fragen pro Runde</span>
<span>·</span>
<span>🔥 Streak-Zähler</span>
<span>·</span>
<span>💡 Bis zu 3 Versuche je Frage</span>
</div>
<button id="startQuizBtn" class="btn btn-primary btn-large btn-launch-quiz" onclick="startGame()">
⚡ Quiz jetzt starten ➔
</button>
</div>
</div>
<!-- Spielbereich (initial versteckt) -->
<div id="gameCard" class="game-area hidden">
<!-- HUD: Fortschritt, Streak, Zeit & Punkte -->
<div class="status-panel">
<div class="progress-info">
<span id="questionCounter" class="hud-pill">Frage 1 / 10</span>
<div class="hud-center-group">
<span id="streakBadge" class="streak-badge" title="Antwort-Serie in Folge">🔥 0</span>
<span id="timerBadge" class="timer-badge">⏱️ 00:00</span>
</div>
<span id="pointsCounter" class="points-badge">🏆 0 Pkt.</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">
<div class="meta-left">
<span id="badgeCategory" class="badge">⚡ Allgemein</span>
<span id="badgeDifficulty" class="badge badge-diff">🟢 Leicht</span>
</div>
<span id="attemptBadge" class="attempt-badge">Versuch 1 / 3</span>
</div>
<h2 id="frageText" class="question-text">Lade nächste Frage...</h2>
<!-- Freitext-Eingabe (wenn question_type == 'text') -->
<div id="textAnswerGroup" class="answer-form">
<input type="text" id="answerInput" class="input-answer" placeholder="Deine Antwort eingeben..." autocomplete="off">
<div class="btn-group">
<button id="checkBtn" class="btn btn-primary" onclick="checkAnswer()">
✓ Antwort prüfen
</button>
<button id="hintBtn" class="btn btn-secondary" onclick="requestHint()">
💡 Tipp anzeigen
</button>
</div>
</div>
<!-- Multiple-Choice-Auswahl (wenn question_type == 'multiple_choice') -->
<div id="choiceAnswerGroup" class="choice-group hidden">
<div id="choiceOptionsContainer" class="choice-grid"></div>
<div class="btn-group" style="margin-top: 16px;">
<button id="hintBtnMC" class="btn btn-secondary" onclick="requestHint()">
💡 Tipp anzeigen
</button>
</div>
</div>
<div id="hintBox" class="hint-box hidden">
<strong id="hintHeading">💡 Hinweis:</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?v=4.0"></script>
{% endblock %}
+10 -8
View File
@@ -5,11 +5,11 @@
{% block content %}
<div class="quiz-wrapper">
<div class="card vorschlag-card">
<h1 class="glow-title" id="suggTitle">✨ Neue Frage für das Quiz vorschlagen ✨</h1>
<p class="subtitle" id="suggSubtitle">Bereichere das magische Quiz mit kniffligen Harry-Potter- oder Escape-Rätseln!</p>
<h1 class="glow-title" id="suggTitle">✍️ Neue Quiz-Frage einreichen</h1>
<p class="subtitle" id="suggSubtitle">Bereichere PotterTrivia mit deinen eigenen Fragen zu Büchern, Filmen oder magischen Geschöpfen!</p>
<div class="info-alert" id="suggAlert">
ℹ️ <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.
ℹ️ <strong>Hinweis zur Moderation:</strong> Eingereichte Fragen werden kurz auf Richtigkeit geprüft, bevor sie im öffentlichen Quiz-Pool freigeschaltet werden.
</div>
<form id="suggestionForm" class="magic-form" onsubmit="submitSuggestion(event)">
@@ -26,13 +26,15 @@
<div class="form-group">
<label for="kategorie" id="suggCatLabel">Kategorie / Themenbereich:</label>
<select id="kategorie" name="category" class="input-text">
<option value="hogwarts" id="catHogwarts">🏰 Hogwarts & Schulgelände</option>
<option value="characters" id="catCharacters" selected>🧙 Charaktere & Personen</option>
<option value="spells" id="catSpells">⚡ Zaubersprüche & Magie</option>
<option value="creatures" id="catCreatures">🐉 Magische Tierwesen</option>
<option value="books" id="catBooks">📖 Buchwissen (Buch-Details)</option>
<option value="movies" id="catMovies">🎬 Filmwissen (Kino & Drehorte)</option>
<option value="creatures" id="catCreatures">🐉 Magische Geschöpfe</option>
<option value="spells" id="catSpells">🪄 Zaubersprüche & Tränke</option>
<option value="characters" id="catCharacters" selected>🧙‍♂️ Charaktere & Personen</option>
<option value="hogwarts" id="catHogwarts">🏰 Hogwarts & Orte</option>
<option value="quidditch" id="catQuidditch">🧹 Quidditch & Sport</option>
<option value="lore" id="catLore">🔮 Artefakte & Geschichte</option>
<option value="general" id="catGeneral">📜 Allgemeines Zauberwissen</option>
<option value="general" id="catGeneral">⚡ Allgemeines Wissen</option>
</select>
</div>
+29
View File
@@ -0,0 +1,29 @@
annotated-doc==0.0.5
annotated-types==0.8.0
anyio==4.15.1
cffi==2.1.1
click==8.5.0
cryptography==50.0.1
fastapi==0.141.1
greenlet==3.5.6
h11==0.16.0
httptools==0.8.0
idna==3.20
itsdangerous==2.2.0
Jinja2==3.1.6
MarkupSafe==3.0.3
pycparser==3.0
pydantic==2.13.5
pydantic_core==2.46.5
PyMySQL==1.2.3
python-dotenv==1.2.3
python-multipart==0.0.32
PyYAML==6.0.3
SQLAlchemy==2.0.54
starlette==1.6.0
typing-inspection==0.4.4
typing_extensions==4.16.0
uvicorn==0.53.0
uvloop==0.22.1
watchfiles==1.3.0
websockets==17.1