Feature: 40 bilingual questions, random 10 game run, difficulty levels, and full i18n support

This commit is contained in:
kobold-ai
2026-09-21 12:33:34 +02:00
parent eafa1f833e
commit cf89cd31c7
15 changed files with 1369 additions and 138 deletions
+87 -26
View File
@@ -16,7 +16,7 @@ from app.models import Question, QuizSession, AdminUser
from app.matcher import is_answer_correct, levenshtein_distance, normalize_text from app.matcher import is_answer_correct, levenshtein_distance, normalize_text
from app.auth import verify_password from app.auth import verify_password
from app.seed import seed_db from app.seed import seed_db
from app.schemas import GameCheckRequest, SuggestionCreateRequest from app.schemas import GameCheckRequest, SuggestionCreateRequest, GameStartRequest
load_dotenv() load_dotenv()
@@ -29,25 +29,33 @@ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
app.mount("/static", StaticFiles(directory=os.path.join(BASE_DIR, "static")), name="static") app.mount("/static", StaticFiles(directory=os.path.join(BASE_DIR, "static")), name="static")
templates = Jinja2Templates(directory=os.path.join(BASE_DIR, "templates")) templates = Jinja2Templates(directory=os.path.join(BASE_DIR, "templates"))
def format_question_for_client(q: Question, number: int, total: int) -> dict: def format_question_for_client(q: Question, number: int, total: int, lang: str = "de") -> dict:
options_list = None options_list = None
if q.question_type == "multiple_choice" and q.options: if lang == "en" and q.question_en:
q_text = q.question_en
opts_raw = q.options_en or q.options
else:
q_text = q.question
opts_raw = q.options
if q.question_type == "multiple_choice" and opts_raw:
try: try:
parsed = json.loads(q.options) parsed = json.loads(opts_raw)
if isinstance(parsed, list): if isinstance(parsed, list):
options_list = list(parsed) options_list = list(parsed)
random.shuffle(options_list) random.shuffle(options_list)
except Exception: except Exception:
options_list = [opt.strip() for opt in q.options.split(",") if opt.strip()] options_list = [opt.strip() for opt in opts_raw.split(",") if opt.strip()]
random.shuffle(options_list) random.shuffle(options_list)
return { return {
"question_number": number, "question_number": number,
"total_questions": total, "total_questions": total,
"category": q.category, "category": q.category,
"question": q.question, "question": q_text,
"question_type": q.question_type or "text", "question_type": q.question_type or "text",
"options": options_list "options": options_list,
"difficulty": q.difficulty or "medium"
} }
@app.on_event("startup") @app.on_event("startup")
@@ -66,13 +74,26 @@ async def index(request: Request, db: Session = Depends(get_db)):
) )
@app.post("/api/game/start") @app.post("/api/game/start")
async def start_game(db: Session = Depends(get_db)): async def start_game(payload: GameStartRequest = None, db: Session = Depends(get_db)):
# Fetch all active questions in order lang = payload.lang if payload and payload.lang in ["de", "en"] else "de"
questions = db.query(Question).filter(Question.status == "active").order_by(Question.sort_order.asc(), Question.id.asc()).all() difficulty = payload.difficulty if payload and payload.difficulty in ["easy", "medium", "hard"] else "all"
if not questions:
raise HTTPException(status_code=400, detail="Keine aktiven Fragen vorhanden.")
question_ids = [q.id for q in questions] # Fetch active questions matching difficulty (or all active)
query = db.query(Question).filter(Question.status == "active")
if difficulty != "all":
query = query.filter(Question.difficulty == difficulty)
pool = query.all()
if not pool:
# Fallback to all active if filtered pool empty
pool = db.query(Question).filter(Question.status == "active").all()
if not pool:
raise HTTPException(status_code=400, detail="Keine aktiven Fragen vorhanden.")
# Randomly sample 10 questions (or all if fewer)
sample_size = min(10, len(pool))
selected_questions = random.sample(pool, sample_size)
question_ids = [q.id for q in selected_questions]
session_id = str(uuid.uuid4()) session_id = str(uuid.uuid4())
new_session = QuizSession( new_session = QuizSession(
@@ -81,18 +102,22 @@ async def start_game(db: Session = Depends(get_db)):
current_question_idx=0, current_question_idx=0,
question_ids=json.dumps(question_ids), question_ids=json.dumps(question_ids),
attempts_current=0, attempts_current=0,
is_finished=False is_finished=False,
lang=lang,
difficulty=difficulty
) )
db.add(new_session) db.add(new_session)
db.commit() db.commit()
first_q = questions[0] first_q = selected_questions[0]
res = format_question_for_client(first_q, 1, len(question_ids)) res = format_question_for_client(first_q, 1, len(question_ids), lang=lang)
res.update({ res.update({
"session_id": session_id, "session_id": session_id,
"score": 0, "score": 0,
"attempts_used": 0, "attempts_used": 0,
"max_attempts": 3 "max_attempts": 3,
"lang": lang,
"difficulty": difficulty
}) })
return res return res
@@ -102,6 +127,7 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db))
if not session or session.is_finished: if not session or session.is_finished:
raise HTTPException(status_code=400, detail="Ungültige oder beendete Spiel-Session.") raise HTTPException(status_code=400, detail="Ungültige oder beendete Spiel-Session.")
session_lang = session.lang or "de"
question_ids = json.loads(session.question_ids) question_ids = json.loads(session.question_ids)
if session.current_question_idx >= len(question_ids): if session.current_question_idx >= len(question_ids):
session.is_finished = True session.is_finished = True
@@ -120,7 +146,7 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db))
if not current_q: if not current_q:
raise HTTPException(status_code=500, detail="Frage nicht in Datenbank gefunden.") raise HTTPException(status_code=500, detail="Frage nicht in Datenbank gefunden.")
correct = is_answer_correct(payload.answer, current_q.canonical_answer, current_q.accepted_answers) correct = is_answer_correct(payload.answer, current_q.canonical_answer, current_q.accepted_answers, current_q.answer_en)
if correct: if correct:
session.score += 1 session.score += 1
@@ -138,7 +164,7 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db))
if not is_finished: if not is_finished:
next_q = db.query(Question).filter(Question.id == question_ids[session.current_question_idx]).first() next_q = db.query(Question).filter(Question.id == question_ids[session.current_question_idx]).first()
if next_q: if next_q:
next_q_data = format_question_for_client(next_q, session.current_question_idx + 1, len(question_ids)) next_q_data = format_question_for_client(next_q, session.current_question_idx + 1, len(question_ids), lang=session_lang)
return { return {
"correct": True, "correct": True,
@@ -154,8 +180,12 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db))
attempts_left = max(0, 3 - session.attempts_current) attempts_left = max(0, 3 - session.attempts_current)
if attempts_left == 0: if attempts_left == 0:
# 3 failed attempts: move to next question, reveal answer so player learns it # 3 failed attempts: move to next question, reveal answer
revealed = current_q.canonical_answer if session_lang == "en" and current_q.answer_en:
revealed = current_q.answer_en
else:
revealed = current_q.canonical_answer
session.current_question_idx += 1 session.current_question_idx += 1
session.attempts_current = 0 session.attempts_current = 0
@@ -170,7 +200,7 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db))
if not is_finished: if not is_finished:
next_q = db.query(Question).filter(Question.id == question_ids[session.current_question_idx]).first() next_q = db.query(Question).filter(Question.id == question_ids[session.current_question_idx]).first()
if next_q: if next_q:
next_q_data = format_question_for_client(next_q, session.current_question_idx + 1, len(question_ids)) next_q_data = format_question_for_client(next_q, session.current_question_idx + 1, len(question_ids), lang=session_lang)
return { return {
"correct": False, "correct": False,
@@ -199,20 +229,29 @@ async def get_hint(session_id: str, db: Session = Depends(get_db)):
if not session or session.is_finished: if not session or session.is_finished:
raise HTTPException(status_code=400, detail="Ungültige Session.") raise HTTPException(status_code=400, detail="Ungültige Session.")
session_lang = session.lang or "de"
question_ids = json.loads(session.question_ids) question_ids = json.loads(session.question_ids)
if session.current_question_idx >= len(question_ids): if session.current_question_idx >= len(question_ids):
return {"hint": "Kein Hinweis verfügbar."} return {"hint": "No hint available." if session_lang == "en" else "Kein Hinweis verfügbar."}
current_q_id = question_ids[session.current_question_idx] current_q_id = question_ids[session.current_question_idx]
current_q = db.query(Question).filter(Question.id == current_q_id).first() current_q = db.query(Question).filter(Question.id == current_q_id).first()
return {"hint": current_q.hint if current_q and current_q.hint else "Kein Tipp verfügbar."} if not current_q:
return {"hint": "No hint available." if session_lang == "en" else "Kein Hinweis verfügbar."}
if session_lang == "en":
hint_text = current_q.hint_en if current_q.hint_en else (current_q.hint or "No hint available.")
else:
hint_text = current_q.hint or "Kein Tipp verfügbar."
return {"hint": hint_text}
@app.get("/finale", response_class=HTMLResponse) @app.get("/finale", response_class=HTMLResponse)
async def finale(request: Request, score: int = 0, total: int = 15): async def finale(request: Request, score: int = 0, total: int = 10, lang: str = "de"):
return templates.TemplateResponse( return templates.TemplateResponse(
request=request, request=request,
name="finale.html", name="finale.html",
context={"score": score, "total": total} context={"score": score, "total": total, "lang": lang}
) )
@app.get("/vorschlag", response_class=HTMLResponse) @app.get("/vorschlag", response_class=HTMLResponse)
@@ -238,6 +277,8 @@ async def submit_suggestion(payload: SuggestionCreateRequest, db: Session = Depe
opts.append(opt.strip()) opts.append(opt.strip())
options_json = json.dumps(opts, ensure_ascii=False) options_json = json.dumps(opts, ensure_ascii=False)
diff = payload.difficulty if payload.difficulty in ["easy", "medium", "hard"] else "medium"
new_suggestion = Question( new_suggestion = Question(
category="community", category="community",
question=payload.frage.strip(), question=payload.frage.strip(),
@@ -245,6 +286,7 @@ async def submit_suggestion(payload: SuggestionCreateRequest, db: Session = Depe
hint=payload.tipp.strip() if payload.tipp else None, hint=payload.tipp.strip() if payload.tipp else None,
question_type=q_type, question_type=q_type,
options=options_json, options=options_json,
difficulty=diff,
status="needs_review", # User Question Need Test / Review status="needs_review", # User Question Need Test / Review
submitted_by=payload.name.strip() if payload.name else "Gast", submitted_by=payload.name.strip() if payload.name else "Gast",
sort_order=999 sort_order=999
@@ -327,6 +369,10 @@ async def admin_add_question(
hint: str = Form(None), hint: str = Form(None),
category: str = Form("harry_potter"), category: str = Form("harry_potter"),
question_type: str = Form("text"), question_type: str = Form("text"),
difficulty: str = Form("medium"),
question_en: str = Form(None),
answer_en: str = Form(None),
hint_en: str = Form(None),
wrong_option_1: str = Form(None), wrong_option_1: str = Form(None),
wrong_option_2: str = Form(None), wrong_option_2: str = Form(None),
wrong_option_3: str = Form(None), wrong_option_3: str = Form(None),
@@ -344,6 +390,8 @@ async def admin_add_question(
opts.append(w.strip()) opts.append(w.strip())
options_json = json.dumps(opts, ensure_ascii=False) options_json = json.dumps(opts, ensure_ascii=False)
diff = difficulty.strip() if difficulty and difficulty.strip() in ["easy", "medium", "hard"] else "medium"
new_q = Question( new_q = Question(
category=category.strip(), category=category.strip(),
question=question.strip(), question=question.strip(),
@@ -351,6 +399,10 @@ async def admin_add_question(
hint=hint.strip() if hint else None, hint=hint.strip() if hint else None,
question_type=question_type.strip(), question_type=question_type.strip(),
options=options_json, options=options_json,
difficulty=diff,
question_en=question_en.strip() if question_en else None,
answer_en=answer_en.strip() if answer_en else None,
hint_en=hint_en.strip() if hint_en else None,
status="active", status="active",
sort_order=sort_order, sort_order=sort_order,
submitted_by=f"Admin ({request.session['admin_user']})" submitted_by=f"Admin ({request.session['admin_user']})"
@@ -402,6 +454,10 @@ async def admin_edit_question(
canonical_answer: str = Form(...), canonical_answer: str = Form(...),
hint: str = Form(None), hint: str = Form(None),
question_type: str = Form("text"), question_type: str = Form("text"),
difficulty: str = Form("medium"),
question_en: str = Form(None),
answer_en: str = Form(None),
hint_en: str = Form(None),
options_raw: str = Form(None), options_raw: str = Form(None),
status_val: str = Form("active"), status_val: str = Form("active"),
sort_order: int = Form(0), sort_order: int = Form(0),
@@ -418,6 +474,11 @@ async def admin_edit_question(
q.canonical_answer = canonical_answer.strip() q.canonical_answer = canonical_answer.strip()
q.hint = hint.strip() if hint else None q.hint = hint.strip() if hint else None
q.question_type = question_type.strip() q.question_type = question_type.strip()
if difficulty and difficulty.strip() in ["easy", "medium", "hard"]:
q.difficulty = difficulty.strip()
q.question_en = question_en.strip() if question_en else None
q.answer_en = answer_en.strip() if answer_en else None
q.hint_en = hint_en.strip() if hint_en else None
if options_raw and options_raw.strip(): if options_raw and options_raw.strip():
val = options_raw.strip() val = options_raw.strip()
+5 -1
View File
@@ -33,12 +33,16 @@ def levenshtein_distance(s1: str, s2: str) -> int:
previous_row = current_row previous_row = current_row
return previous_row[-1] return previous_row[-1]
def is_answer_correct(user_input: str, canonical: str, accepted_raw: str | None = None) -> bool: def is_answer_correct(user_input: str, canonical: str, accepted_raw: str | None = None, canonical_en: str | None = None) -> bool:
clean_user = normalize_text(user_input) clean_user = normalize_text(user_input)
if not clean_user: if not clean_user:
return False return False
targets = [normalize_text(canonical)] targets = [normalize_text(canonical)]
if canonical_en:
clean_en = normalize_text(canonical_en)
if clean_en and clean_en not in targets:
targets.append(clean_en)
if accepted_raw: if accepted_raw:
try: try:
+14 -2
View File
@@ -7,12 +7,22 @@ class Question(Base):
id = Column(Integer, primary_key=True, index=True, autoincrement=True) id = Column(Integer, primary_key=True, index=True, autoincrement=True)
category = Column(String(50), default="harry_potter", index=True) category = Column(String(50), default="harry_potter", index=True)
difficulty = Column(String(20), default="medium", index=True) # easy, medium, hard
question_type = Column(String(20), default="text") # 'text' or 'multiple_choice'
# German content (default)
question = Column(Text, nullable=False) question = Column(Text, nullable=False)
canonical_answer = Column(String(255), nullable=False) canonical_answer = Column(String(255), nullable=False)
accepted_answers = Column(Text, nullable=True) # JSON or comma-delimited
question_type = Column(String(20), default="text") # 'text' or 'multiple_choice'
options = Column(Text, nullable=True) # JSON list of options for multiple choice options = Column(Text, nullable=True) # JSON list of options for multiple choice
hint = Column(Text, nullable=True) hint = Column(Text, nullable=True)
# English content
question_en = Column(Text, nullable=True)
answer_en = Column(String(255), nullable=True)
options_en = Column(Text, nullable=True) # JSON list of options in English
hint_en = Column(Text, nullable=True)
accepted_answers = Column(Text, nullable=True) # JSON or comma-delimited
status = Column(String(30), default="active", index=True) # active, needs_review, rejected, archived status = Column(String(30), default="active", index=True) # active, needs_review, rejected, archived
submitted_by = Column(String(100), nullable=True) submitted_by = Column(String(100), nullable=True)
sort_order = Column(Integer, default=0) sort_order = Column(Integer, default=0)
@@ -29,6 +39,8 @@ class QuizSession(Base):
question_ids = Column(Text, nullable=False) # JSON array string: "[1, 2, 3...]" question_ids = Column(Text, nullable=False) # JSON array string: "[1, 2, 3...]"
attempts_current = Column(Integer, default=0) attempts_current = Column(Integer, default=0)
is_finished = Column(Boolean, default=False) is_finished = Column(Boolean, default=False)
lang = Column(String(10), default="de") # 'de' or 'en'
difficulty = Column(String(20), default="all") # 'all', 'easy', 'medium', 'hard'
created_at = Column(DateTime, default=datetime.datetime.utcnow) created_at = Column(DateTime, default=datetime.datetime.utcnow)
finished_at = Column(DateTime, nullable=True) finished_at = Column(DateTime, nullable=True)
+7
View File
@@ -1,6 +1,10 @@
from pydantic import BaseModel from pydantic import BaseModel
from typing import Optional, List from typing import Optional, List
class GameStartRequest(BaseModel):
lang: Optional[str] = "de"
difficulty: Optional[str] = "all"
class GameStartResponse(BaseModel): class GameStartResponse(BaseModel):
session_id: str session_id: str
question_number: int question_number: int
@@ -10,6 +14,8 @@ class GameStartResponse(BaseModel):
score: int score: int
attempts_used: int attempts_used: int
max_attempts: int = 3 max_attempts: int = 3
difficulty: Optional[str] = "medium"
lang: Optional[str] = "de"
class GameCheckRequest(BaseModel): class GameCheckRequest(BaseModel):
session_id: str session_id: str
@@ -32,6 +38,7 @@ class SuggestionCreateRequest(BaseModel):
name: Optional[str] = "Gast" name: Optional[str] = "Gast"
question_type: Optional[str] = "text" question_type: Optional[str] = "text"
falsche_antworten: Optional[List[str]] = None falsche_antworten: Optional[List[str]] = None
difficulty: Optional[str] = "medium"
class QuestionAdminCreate(BaseModel): class QuestionAdminCreate(BaseModel):
category: str = "harry_potter" category: str = "harry_potter"
+6 -46
View File
@@ -3,6 +3,7 @@ import json
from app.database import engine, SessionLocal, Base from app.database import engine, SessionLocal, Base
from app.models import Question, AdminUser from app.models import Question, AdminUser
from app.auth import hash_password from app.auth import hash_password
from app.seed_40 import seed_40_questions
def seed_db(): def seed_db():
Base.metadata.create_all(bind=engine) Base.metadata.create_all(bind=engine)
@@ -22,52 +23,11 @@ def seed_db():
db.commit() db.commit()
print(f"Created default admin user: {admin_user}") print(f"Created default admin user: {admin_user}")
# 2. Existing Questions from legacy/fragen.json # 2. Seed 40 questions if database is empty
legacy_questions_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "legacy", "fragen.json") question_count = db.query(Question).count()
if os.path.exists(legacy_questions_path): if question_count == 0:
with open(legacy_questions_path, "r", encoding="utf-8") as f: print("Question table is empty, seeding 40 initial bilingual questions...")
legacy_questions = json.load(f) seed_40_questions()
count = 0
for idx, q in enumerate(legacy_questions):
# Check if question text already in DB
existing = db.query(Question).filter(Question.question == q.get("frage", "").strip()).first()
if not existing:
db.add(Question(
category="harry_potter",
question=q.get("frage", "").strip(),
canonical_answer=q.get("antwort", "").strip(),
hint=q.get("tipp", "").strip(),
status="active",
sort_order=idx + 1,
submitted_by="Admin"
))
count += 1
db.commit()
print(f"Imported {count} active questions from legacy/fragen.json")
# 3. Existing Suggestions from legacy/fragen_vorschlaege.json
legacy_sugg_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "legacy", "fragen_vorschlaege.json")
if os.path.exists(legacy_sugg_path):
with open(legacy_sugg_path, "r", encoding="utf-8") as f:
legacy_sugg = json.load(f)
count_sugg = 0
for q in legacy_sugg:
existing = db.query(Question).filter(Question.question == q.get("frage", "").strip()).first()
if not existing:
db.add(Question(
category="harry_potter",
question=q.get("frage", "").strip(),
canonical_answer=q.get("antwort", "").strip(),
hint=q.get("tipp", "").strip(),
status="needs_review",
sort_order=999,
submitted_by="Gast (Legacy Vorschlag)"
))
count_sugg += 1
db.commit()
print(f"Imported {count_sugg} pending suggestions from legacy/fragen_vorschlaege.json")
finally: finally:
db.close() db.close()
+609
View File
@@ -0,0 +1,609 @@
import json
from app.database import engine, SessionLocal
from app.models import Question
from sqlalchemy import text
QUESTIONS_DATA = [
# --- EASY (1-14) ---
{
"category": "creatures",
"difficulty": "easy",
"question_type": "multiple_choice",
"question": "Wie heißt Harrys schneeweiße Eule?",
"canonical_answer": "Hedwig",
"options": ["Hedwig", "Errol", "Pigwidgeon", "Fawkes"],
"hint": "Harry bekam sie von Hagrid in der Winkelgasse geschenkt.",
"question_en": "What is the name of Harry's snowy pet owl?",
"answer_en": "Hedwig",
"options_en": ["Hedwig", "Errol", "Pigwidgeon", "Fawkes"],
"hint_en": "Harry received her as a gift from Hagrid in Diagon Alley."
},
{
"category": "hogwarts",
"difficulty": "easy",
"question_type": "multiple_choice",
"question": "Welche Farben repräsentieren das Haus Gryffindor?",
"canonical_answer": "Rot und Gold",
"options": ["Rot und Gold", "Grün und Silber", "Blau und Bronze", "Gelb und Schwarz"],
"hint": "Sie erinnern an ein loderndes Löwenfeuer.",
"question_en": "Which colours represent the House of Gryffindor?",
"answer_en": "Red and Gold",
"options_en": ["Red and Gold", "Green and Silver", "Blue and Bronze", "Yellow and Black"],
"hint_en": "They evoke the fiery colours of a proud lion."
},
{
"category": "hogwarts",
"difficulty": "easy",
"question_type": "text",
"question": "Von welchem Gleis am Bahnhof King's Cross fährt der Hogwarts-Express ab?",
"canonical_answer": "9 3/4",
"options": None,
"hint": "Eine Bruchzahl zwischen Bahnsteig 9 und 10.",
"question_en": "From which platform at King's Cross station does the Hogwarts Express depart?",
"answer_en": "9 3/4",
"options_en": None,
"hint_en": "A fraction between platforms 9 and 10."
},
{
"category": "quidditch",
"difficulty": "easy",
"question_type": "text",
"question": "Wie viele Punkte bringt das Fangen des Goldenen Schnatzes im Quidditch?",
"canonical_answer": "150",
"options": None,
"hint": "Mehr als einhundert Punkte.",
"question_en": "How many points is catching the Golden Snitch worth in Quidditch?",
"answer_en": "150",
"options_en": None,
"hint_en": "More than one hundred points."
},
{
"category": "hogwarts",
"difficulty": "easy",
"question_type": "text",
"question": "In wie viele verschiedene Häuser teilt der Sprechende Hut die Schüler ein?",
"canonical_answer": "4",
"options": None,
"hint": "Gryffindor, Hufflepuff, Ravenclaw und Slytherin.",
"question_en": "Into how many different houses does the Sorting Hat divide students?",
"answer_en": "4",
"options_en": None,
"hint_en": "Gryffindor, Hufflepuff, Ravenclaw, and Slytherin."
},
{
"category": "spells",
"difficulty": "easy",
"question_type": "multiple_choice",
"question": "Mit welchem Zauberspruch beschwört man einen Patronus herauf?",
"canonical_answer": "Expecto Patronum",
"options": ["Expecto Patronum", "Expelliarmus", "Lumos Solem", "Avada Kedavra"],
"hint": "Er erfordert eine zutiefst glückliche Erinnerung.",
"question_en": "Which incantation conjures a Patronus guardian?",
"answer_en": "Expecto Patronum",
"options_en": ["Expecto Patronum", "Expelliarmus", "Lumos Solem", "Avada Kedavra"],
"hint_en": "It requires a powerfully happy memory."
},
{
"category": "characters",
"difficulty": "easy",
"question_type": "text",
"question": "Wie lautet der zweite Vorname von Harry Potter?",
"canonical_answer": "James",
"options": None,
"hint": "Er trägt denselben Namen wie sein Vater.",
"question_en": "What is Harry Potter's middle name?",
"answer_en": "James",
"options_en": None,
"hint_en": "He shares this name with his father."
},
{
"category": "lore",
"difficulty": "easy",
"question_type": "multiple_choice",
"question": "Welcher magische Kern befindet sich im Zauberstab von Harry Potter?",
"canonical_answer": "Phönixfeder",
"options": ["Phönixfeder", "Drachenherzfaser", "Einhornhaar", "Veela-Haar"],
"hint": "Die Feder stammt von Dumbledores Vogel Fawkes.",
"question_en": "What magical core is inside Harry Potter's wand?",
"answer_en": "Phoenix feather",
"options_en": ["Phoenix feather", "Dragon heartstring", "Unicorn hair", "Veela hair"],
"hint_en": "The feather was donated by Dumbledore's phoenix Fawkes."
},
{
"category": "characters",
"difficulty": "easy",
"question_type": "text",
"question": "Wer war der weise Schulleiter von Hogwarts zu Harrys Schulzeit?",
"canonical_answer": "Albus Dumbledore",
"options": None,
"hint": "Er trägt eine Halbmondbrille und liebt Zitronenbrausebonbons.",
"question_en": "Who was the wise Headmaster of Hogwarts during most of Harry's schooling?",
"answer_en": "Albus Dumbledore",
"options_en": None,
"hint_en": "He wears half-moon spectacles and loves Lemon Drops."
},
{
"category": "characters",
"difficulty": "easy",
"question_type": "text",
"question": "Wie heißt der Zwillingsbruder von Fred Weasley?",
"canonical_answer": "George",
"options": None,
"hint": "Gemeinsam eröffneten sie 'Weasleys Zauberhafte Zauberscherze'.",
"question_en": "What is the name of Fred Weasley's twin brother?",
"answer_en": "George",
"options_en": None,
"hint_en": "Together they opened 'Weasleys' Wizard Wheezes'."
},
{
"category": "hogwarts",
"difficulty": "easy",
"question_type": "multiple_choice",
"question": "Welches Tier ist das Wappentier des Hauses Hufflepuff?",
"canonical_answer": "Dachs",
"options": ["Dachs", "Biber", "Eichhörnchen", "Otter"],
"hint": "Ein grabendes, treues und wehrhaftes Tier.",
"question_en": "Which animal is the emblem of House Hufflepuff?",
"answer_en": "Badger",
"options_en": ["Badger", "Beaver", "Squirrel", "Otter"],
"hint_en": "A loyal, hard-working, burrowing animal."
},
{
"category": "spells",
"difficulty": "easy",
"question_type": "text",
"question": "Welcher Zauberspruch entwaffnet einen gegnerischen Zauberer?",
"canonical_answer": "Expelliarmus",
"options": None,
"hint": "Harrys Markenzeichen im Duell.",
"question_en": "Which spell is used to disarm an opponent?",
"answer_en": "Expelliarmus",
"options_en": None,
"hint_en": "Harry's trademark defensive spell in duels."
},
{
"category": "characters",
"difficulty": "easy",
"question_type": "multiple_choice",
"question": "In welches Tier kann sich Sirius Black als Animagus verwandeln?",
"canonical_answer": "Schwarzer Hund",
"options": ["Schwarzer Hund", "Wolf", "Hirsch", "Ratte"],
"hint": "Sein Spitzname lautet 'Tatze' (Padfoot).",
"question_en": "Which animal does Sirius Black transform into as an Animagus?",
"answer_en": "Black Dog",
"options_en": ["Black Dog", "Wolf", "Stag", "Rat"],
"hint_en": "His Marauder nickname is 'Padfoot'."
},
{
"category": "lore",
"difficulty": "easy",
"question_type": "text",
"question": "Wie viele Heiligtümer des Todes gibt es laut der Legende der drei Brüder?",
"canonical_answer": "3",
"options": None,
"hint": "Elderstab, Stein der Auferstehung und Tarnumhang.",
"question_en": "How many Deathly Hallows exist according to the Tale of the Three Brothers?",
"answer_en": "3",
"options_en": None,
"hint_en": "The Elder Wand, the Resurrection Stone, and the Invisibility Cloak."
},
# --- MEDIUM (15-30) ---
{
"category": "lore",
"difficulty": "medium",
"question_type": "multiple_choice",
"question": "Wie heißt die beliebte Wirtin des Gasthauses 'Drei Besen' in Hogsmeade?",
"canonical_answer": "Madam Rosmerta",
"options": ["Madam Rosmerta", "Madam Malkin", "Madam Pince", "Madam Pomfrey"],
"hint": "Ron Weasley hat eine kleine Schwäche für sie.",
"question_en": "What is the name of the popular landlady of 'The Three Broomsticks' in Hogsmeade?",
"answer_en": "Madam Rosmerta",
"options_en": ["Madam Rosmerta", "Madam Malkin", "Madam Pince", "Madam Pomfrey"],
"hint_en": "Ron Weasley harbors quite a crush on her."
},
{
"category": "creatures",
"difficulty": "medium",
"question_type": "text",
"question": "Welche riesige Schlange hauste tief unter Hogwarts in der Kammer des Schreckens?",
"canonical_answer": "Basilisk",
"options": None,
"hint": "Ihr Blick ist absolut tödlich.",
"question_en": "Which giant serpent dwelt deep beneath Hogwarts in the Chamber of Secrets?",
"answer_en": "Basilisk",
"options_en": None,
"hint_en": "Its direct stare causes instant death."
},
{
"category": "creatures",
"difficulty": "medium",
"question_type": "text",
"question": "Wie heißt Albus Dumbledores treuer Phönix?",
"canonical_answer": "Fawkes",
"options": None,
"hint": "Seine Tränen besitzen wundersame Heilkräfte.",
"question_en": "What is the name of Albus Dumbledore's loyal pet phoenix?",
"answer_en": "Fawkes",
"options_en": None,
"hint_en": "His tears have miraculous healing powers."
},
{
"category": "characters",
"difficulty": "medium",
"question_type": "text",
"question": "Wie heißt der mürrische alte Hauself der Familie Black am Grimmauldplatz 12?",
"canonical_answer": "Kreacher",
"options": None,
"hint": "Er verehrte Regulus Black über alles.",
"question_en": "What is the name of the grumpy house-elf serving the Black family at 12 Grimmauld Place?",
"answer_en": "Kreacher",
"options_en": None,
"hint_en": "He was fiercely loyal to Regulus Black."
},
{
"category": "characters",
"difficulty": "medium",
"question_type": "multiple_choice",
"question": "Welchen Spitznamen trug Remus Lupin als Schöpfer der Karte des Rumtreibers?",
"canonical_answer": "Moony",
"options": ["Moony", "Wurmschwanz", "Tatze", "Krone"],
"hint": "Eine Anspielung auf den Vollmond.",
"question_en": "What was Remus Lupin's Marauder nickname?",
"answer_en": "Moony",
"options_en": ["Moony", "Wormtail", "Padfoot", "Prongs"],
"hint_en": "A direct reference to the full moon."
},
{
"category": "creatures",
"difficulty": "medium",
"question_type": "multiple_choice",
"question": "Wie heißt Hermines krummbeiniger roter Kater mit Kniesel-Abstammung?",
"canonical_answer": "Krummbein",
"options": ["Krummbein", "Knut", "Knecht Ruprecht", "Klaue"],
"hint": "Auf Englisch heißt er Crookshanks.",
"question_en": "What is the name of Hermione's half-Kneazle ginger cat?",
"answer_en": "Crookshanks",
"options_en": ["Crookshanks", "Krummbein", "Claw", "Rusty"],
"hint_en": "He immediately recognized Scabbers as an imposter."
},
{
"category": "quidditch",
"difficulty": "medium",
"question_type": "text",
"question": "Wie viele Bälle sind insgesamt gleichzeitig bei einem regulären Quidditch-Spiel im Einsatz?",
"canonical_answer": "4",
"options": None,
"hint": "Ein Quaffel, zwei Klatscher und ein Schnatz.",
"question_en": "How many total balls are in play during a regular Quidditch match?",
"answer_en": "4",
"options_en": None,
"hint_en": "One Quaffle, two Bludgers, and one Golden Snitch."
},
{
"category": "spells",
"difficulty": "medium",
"question_type": "multiple_choice",
"question": "Mit welchem Spruch wird ein gestaltwandelnder Irrwicht unschädlich gemacht?",
"canonical_answer": "Riddikulus",
"options": ["Riddikulus", "Reducto", "Rictusempra", "Relashio"],
"hint": "Man muss sich etwas Lächerliches und Heiteres vorstellen.",
"question_en": "Which charm dispels a shape-shifting Boggart by turning it comical?",
"answer_en": "Riddikulus",
"options_en": ["Riddikulus", "Reducto", "Rictusempra", "Relashio"],
"hint_en": "It requires turning the creature into something funny."
},
{
"category": "spells",
"difficulty": "medium",
"question_type": "multiple_choice",
"question": "Welcher Zauberspruch lässt Gegenstände in die Luft schweben?",
"canonical_answer": "Wingardium Leviosa",
"options": ["Wingardium Leviosa", "Accio", "Alohomora", "Avis"],
"hint": "'Es heißt Leviooooosa, nicht Leviosaaaa!'",
"question_en": "Which levitation charm makes objects fly into the air?",
"answer_en": "Wingardium Leviosa",
"options_en": ["Wingardium Leviosa", "Accio", "Alohomora", "Avis"],
"hint_en": "'It's Levi-ooo-sa, not Levio-saaa!' - Hermione Granger."
},
{
"category": "creatures",
"difficulty": "medium",
"question_type": "multiple_choice",
"question": "Gegen welche gefährliche Drachenart musste Harry in der ersten Aufgabe des Trimagischen Turniers antreten?",
"canonical_answer": "Ungarischer Hornschwanz",
"options": ["Ungarischer Hornschwanz", "Schwedischer Kurzschnäuzler", "Gemeiner Walisischer Grünling", "Chinesischer Feuerball"],
"hint": "Der gefährlichste und bösartigste aller vier Drachen.",
"question_en": "Which fierce dragon breed did Harry face in the First Task of the Triwizard Tournament?",
"answer_en": "Hungarian Horntail",
"options_en": ["Hungarian Horntail", "Swedish Short-Snout", "Common Welsh Green", "Chinese Fireball"],
"hint_en": "Considered the most vicious and dangerous dragon of all four."
},
{
"category": "hogwarts",
"difficulty": "medium",
"question_type": "multiple_choice",
"question": "Welches Tier ziert das offizielle Hauswappen von Ravenclaw?",
"canonical_answer": "Adler",
"options": ["Adler", "Rabe", "Falke", "Eule"],
"hint": "Entgegen dem Namensbestandteil 'Raven' ist es kein Rabe!",
"question_en": "Which bird is the official emblem of House Ravenclaw in the books?",
"answer_en": "Eagle",
"options_en": ["Eagle", "Raven", "Falcon", "Owl"],
"hint_en": "Despite the house name, it is definitely not a raven!"
},
{
"category": "hogwarts",
"difficulty": "medium",
"question_type": "multiple_choice",
"question": "Wie heißt der geheimnisvolle Raum im 7. Stock, der nur erscheint, wenn man ihn wirklich braucht?",
"canonical_answer": "Raum der Wünsche",
"options": ["Raum der Wünsche", "Raum der Rätsel", "Spiegelkammer", "Halle der Prophezeiungen"],
"hint": "Dumbledores Armee trainierte hier heimlich.",
"question_en": "What is the name of the magical 7th-floor room that only appears when someone is in real need of it?",
"answer_en": "Room of Requirement",
"options_en": ["Room of Requirement", "Chamber of Riddles", "Hall of Prophecy", "Mirror Room"],
"hint_en": "Dumbledore's Army used it as their secret training ground."
},
{
"category": "characters",
"difficulty": "medium",
"question_type": "multiple_choice",
"question": "Welche Gestalt nahm der Patronus von Severus Snape an?",
"canonical_answer": "Silberne Hirschkuh",
"options": ["Silberne Hirschkuh", "Großer Hirsch", "Schlange", "Fledermaus"],
"hint": "'Nach all dieser Zeit?' – 'Immer.'",
"question_en": "What shape did Severus Snape's Patronus take?",
"answer_en": "Silver Doe",
"options_en": ["Silver Doe", "Stag", "Serpent", "Bat"],
"hint_en": "'After all this time?' – 'Always.'"
},
{
"category": "lore",
"difficulty": "medium",
"question_type": "multiple_choice",
"question": "Welcher Horkrux von Lord Voldemort wurde als allererstes von Harry Potter zerstört?",
"canonical_answer": "Tom Riddles Tagebuch",
"options": ["Tom Riddles Tagebuch", "Medaillon von Slytherin", "Ring der Gaunts", "Diadem von Ravenclaw"],
"hint": "In der Kammer des Schreckens mit einem Basiliskenzahn.",
"question_en": "Which of Voldemort's Horcruxes was the very first to be destroyed?",
"answer_en": "Tom Riddle's Diary",
"options_en": ["Tom Riddle's Diary", "Slytherin's Locket", "Gaunt's Ring", "Ravenclaw's Diadem"],
"hint_en": "Destroyed in the Chamber of Secrets with a basilisk fang."
},
{
"category": "characters",
"difficulty": "medium",
"question_type": "multiple_choice",
"question": "Wie lautet der vollständige Vorname des weltberühmten Zauberstabmachers Ollivander?",
"canonical_answer": "Garrick",
"options": ["Garrick", "Gerbold", "Gervase", "Gellert"],
"hint": "Sein Geschäft in der Winkelgasse besteht seit 382 v. Chr.",
"question_en": "What is the first name of the master wandmaker Mr. Ollivander?",
"answer_en": "Garrick",
"options_en": ["Garrick", "Gerbold", "Gervase", "Gellert"],
"hint_en": "Makers of fine wands since 382 B.C."
},
{
"category": "characters",
"difficulty": "medium",
"question_type": "text",
"question": "Wie lautet der echte Vorname von Auror 'Mad-Eye' Moody?",
"canonical_answer": "Alastor",
"options": None,
"hint": "Beginnt mit dem Buchstaben A.",
"question_en": "What is the actual first name of Auror 'Mad-Eye' Moody?",
"answer_en": "Alastor",
"options_en": None,
"hint_en": "Starts with the letter A."
},
# --- HARD (31-40) ---
{
"category": "characters",
"difficulty": "hard",
"question_type": "multiple_choice",
"question": "Wer war der Schulleiter von Hogwarts unmittelbar vor Albus Dumbledore?",
"canonical_answer": "Armando Dippet",
"options": ["Armando Dippet", "Phineas Nigellus Black", "Dexter Fortescue", "Dilys Derwent"],
"hint": "Er war Schulleiter, als Tom Riddle 1943 die Kammer des Schreckens öffnete.",
"question_en": "Who served as Headmaster of Hogwarts immediately before Albus Dumbledore?",
"answer_en": "Armando Dippet",
"options_en": ["Armando Dippet", "Phineas Nigellus Black", "Dexter Fortescue", "Dilys Derwent"],
"hint_en": "He was in charge when Tom Riddle opened the Chamber of Secrets in 1943."
},
{
"category": "characters",
"difficulty": "hard",
"question_type": "text",
"question": "Wie hieß die jüngere Schwester von Albus und Aberforth Dumbledore, die tragisch verstarb?",
"canonical_answer": "Ariana",
"options": None,
"hint": "Ihr unkontrollierter Zauberausbruch tötete einst ihre Mutter Kendra.",
"question_en": "What was the name of Albus and Aberforth Dumbledore's tragic younger sister?",
"answer_en": "Ariana",
"options_en": None,
"hint_en": "She was traumatized as a child and died during a three-way duel."
},
{
"category": "characters",
"difficulty": "hard",
"question_type": "text",
"question": "Wie hieß die Mutter von Tom Marvolo Riddle (Lord Voldemort) mit Vornamen?",
"canonical_answer": "Merope",
"options": None,
"hint": "Merope Gaunt, eine Nachfahrin von Salazar Slytherin.",
"question_en": "What was the first name of Tom Riddle's (Lord Voldemort's) pure-blood mother?",
"answer_en": "Merope",
"options_en": None,
"hint_en": "Merope Gaunt, direct descendant of Salazar Slytherin."
},
{
"category": "lore",
"difficulty": "hard",
"question_type": "multiple_choice",
"question": "In welcher exakten Reihenfolge werden die vier Schöpfer auf der Karte des Rumtreibers genannt?",
"canonical_answer": "Moony, Wurmschwanz, Tatze und Krone",
"options": [
"Moony, Wurmschwanz, Tatze und Krone",
"Krone, Tatze, Wurmschwanz und Moony",
"Moony, Tatze, Wurmschwanz und Krone",
"Tatze, Moony, Wurmschwanz und Krone"
],
"hint": "Moony steht ganz am Anfang.",
"question_en": "In what exact order are the four Marauders listed on the Marauder's Map?",
"answer_en": "Moony, Wormtail, Padfoot, and Prongs",
"options_en": [
"Moony, Wormtail, Padfoot, and Prongs",
"Prongs, Padfoot, Wormtail, and Moony",
"Moony, Padfoot, Wormtail, and Prongs",
"Padfoot, Moony, Wormtail, and Prongs"
],
"hint_en": "Moony is listed first, reflecting the reverse order of their deaths."
},
{
"category": "lore",
"difficulty": "hard",
"question_type": "multiple_choice",
"question": "Wie lange muss der Vielsafttrank laut Rezeptur mindestens im Kessel köcheln?",
"canonical_answer": "Einen Monat",
"options": ["Einen Monat", "Zwei Wochen", "Sieben Tage", "Drei Monate"],
"hint": "Unter anderem müssen Florfliegen 21 Tage lang gedünstet werden.",
"question_en": "How long must the complex Polyjuice Potion simmer before it is ready to drink?",
"answer_en": "One month",
"options_en": ["One month", "Two weeks", "Seven days", "Three months"],
"hint_en": "Lacewing flies alone must be stewed for 21 days."
},
{
"category": "spells",
"difficulty": "hard",
"question_type": "text",
"question": "Mit welchem lateinischstämmigen Zauber versiegelt man Türen gegen unbefugtes Betreten?",
"canonical_answer": "Colloportus",
"options": None,
"hint": "Erzeugt ein eigenartiges matschiges Sauggeräusch beim Schließen.",
"question_en": "Which locking spell seals doors securely, making an odd squelching noise?",
"answer_en": "Colloportus",
"options_en": None,
"hint_en": "Frequently used by Hermione in the Department of Mysteries."
},
{
"category": "characters",
"difficulty": "hard",
"question_type": "text",
"question": "Wie lautet der Nachname der strengen Bibliothekarin der verbotenen Abteilung in Hogwarts?",
"canonical_answer": "Madam Pince",
"options": None,
"hint": "Ihr Vorname ist Irma, ihr Nachname klingt wie 'zwicken' auf Englisch.",
"question_en": "What is the surname of Hogwarts' notoriously strict head librarian?",
"answer_en": "Madam Pince",
"options_en": None,
"hint_en": "Her first name is Irma; she guards the Restricted Section fiercely."
},
{
"category": "lore",
"difficulty": "hard",
"question_type": "multiple_choice",
"question": "Welcher Blutstatus trifft biologisch auf Lord Voldemort zu?",
"canonical_answer": "Halbblut",
"options": ["Halbblut", "Reinblut", "Muggelgeboren", "Squib"],
"hint": "Sein Vater Tom Riddle Senior war ein gewöhnlicher Muggel.",
"question_en": "What was Lord Voldemort's actual biological blood status?",
"answer_en": "Half-blood",
"options_en": ["Half-blood", "Pure-blood", "Muggle-born", "Squib"],
"hint_en": "His father Tom Riddle Sr. was an ordinary Muggle."
},
{
"category": "quidditch",
"difficulty": "hard",
"question_type": "multiple_choice",
"question": "Auf welcher Position spielte Harrys Vater James Potter im Gryffindor-Quidditch-Team?",
"canonical_answer": "Jäger",
"options": ["Jäger", "Sucher", "Treiber", "Hüter"],
"hint": "Obwohl er im Film mit einem Schnatz spielte, war er in den Büchern Jäger (Chaser).",
"question_en": "Which position did Harry's father James Potter play on the Gryffindor Quidditch team in the books?",
"answer_en": "Chaser",
"options_en": ["Chaser", "Seeker", "Beater", "Keeper"],
"hint_en": "Though often seen fiddling with a Snitch, J.K. Rowling confirmed he was a Chaser."
},
{
"category": "lore",
"difficulty": "hard",
"question_type": "multiple_choice",
"question": "Was bedeutet der rückwärts gelesene Spruch auf dem Spiegel Nerhegeb ('Erised stra ehru oyt ube cafru oyt on wohsi')?",
"canonical_answer": "Ich zeige nicht dein Gesicht, sondern deines Herzens Verlangen",
"options": [
"Ich zeige nicht dein Gesicht, sondern deines Herzens Verlangen",
"Nur wer die Wahrheit sucht, wird seine Zukunft sehen",
"Der Blick in die Vergangenheit heilt keine Wunden",
"Wer mich erblickt, verliert den Verstand an seine Träume"
],
"hint": "'Nerhegeb' rückwärts gelesen ergibt 'Begehren'.",
"question_en": "What does the reversed inscription on the Mirror of Erised mean?",
"answer_en": "I show not your face but your heart's desire",
"options_en": [
"I show not your face but your heart's desire",
"Only the pure of heart may see their future here",
"Look not into the past lest you lose your present",
"He who gazes within shall drown in their own dreams"
],
"hint_en": "'Erised' backwards spells 'Desire'."
}
]
def seed_40():
db = SessionLocal()
try:
# 1. Clear all existing questions as requested
print("Clearing existing questions...")
db.query(Question).delete()
db.commit()
# 2. Insert the 40 new rich bilingual questions
print(f"Inserting {len(QUESTIONS_DATA)} new bilingual questions...")
for idx, q_data in enumerate(QUESTIONS_DATA, 1):
options_json = json.dumps(q_data["options"], ensure_ascii=False) if q_data["options"] else None
options_en_json = json.dumps(q_data["options_en"], ensure_ascii=False) if q_data["options_en"] else None
# Accepted answers list includes both German and English answers
accepted = [q_data["canonical_answer"], q_data["answer_en"]]
# Also common variations
if q_data["canonical_answer"] == "9 3/4":
accepted.extend(["9 3/4", "9 3/4", "9,75", "neun dreiviertel", "nine and three quarters"])
elif q_data["canonical_answer"] == "Albus Dumbledore":
accepted.append("Dumbledore")
elif q_data["canonical_answer"] == "Tom Riddles Tagebuch":
accepted.extend(["Tagebuch", "Diary", "Riddles Tagebuch"])
accepted_json = json.dumps(list(set(accepted)), ensure_ascii=False)
new_q = Question(
id=idx,
category=q_data["category"],
difficulty=q_data["difficulty"],
question_type=q_data["question_type"],
question=q_data["question"],
canonical_answer=q_data["canonical_answer"],
options=options_json,
hint=q_data["hint"],
question_en=q_data["question_en"],
answer_en=q_data["answer_en"],
options_en=options_en_json,
hint_en=q_data["hint_en"],
accepted_answers=accepted_json,
status="active",
sort_order=idx,
submitted_by="Zaubereiministerium"
)
db.add(new_q)
db.commit()
print("Successfully seeded 40 bilingual questions across Easy, Medium, and Hard!")
finally:
db.close()
seed_40_questions = seed_40
if __name__ == "__main__":
seed_40()
+127
View File
@@ -83,6 +83,41 @@ body {
gap: 10px; gap: 10px;
} }
.lang-switch-group {
display: inline-flex;
background: rgba(20, 22, 40, 0.85);
border: 1px solid var(--border-gold);
border-radius: 20px;
padding: 2px;
overflow: hidden;
}
.lang-btn {
background: transparent;
border: none;
color: var(--text-muted);
font-size: 0.8rem;
font-weight: 600;
padding: 4px 9px;
border-radius: 16px;
cursor: pointer;
transition: all 0.2s ease;
min-height: 28px;
display: flex;
align-items: center;
gap: 4px;
}
.lang-btn:hover {
color: var(--gold);
}
.lang-btn.active {
background: var(--gold);
color: #0b0b16;
box-shadow: 0 0 8px rgba(201, 168, 106, 0.4);
}
.audio-btn { .audio-btn {
background: rgba(201, 168, 106, 0.15); background: rgba(201, 168, 106, 0.15);
border: 1px solid var(--gold); border: 1px solid var(--gold);
@@ -302,6 +337,57 @@ body {
color: var(--text-muted); color: var(--text-muted);
} }
.difficulty-selection {
margin: 18px 0 22px;
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
.difficulty-label {
font-size: 0.95rem;
color: var(--gold-soft);
font-weight: 600;
}
.difficulty-chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: center;
}
.diff-chip {
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--border-gold);
color: var(--text-muted);
border-radius: 20px;
padding: 8px 16px;
font-size: 0.88rem;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 6px;
transition: all 0.2s ease;
font-family: inherit;
}
.diff-chip:hover {
border-color: var(--gold);
color: var(--text-main);
transform: translateY(-1px);
background: rgba(201, 168, 106, 0.12);
}
.diff-chip.active {
background: rgba(201, 168, 106, 0.25);
border-color: var(--gold);
color: var(--gold-bright);
box-shadow: 0 0 10px rgba(201, 168, 106, 0.35);
font-weight: bold;
}
/* ========================================= /* =========================================
BUTTONS BUTTONS
========================================= */ ========================================= */
@@ -446,6 +532,13 @@ body {
gap: 8px; gap: 8px;
} }
.meta-left {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.badge { .badge {
padding: 4px 10px; padding: 4px 10px;
border-radius: 12px; border-radius: 12px;
@@ -457,6 +550,30 @@ body {
white-space: nowrap; white-space: nowrap;
} }
.badge-diff {
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.2);
color: var(--text-main);
}
.badge-diff-easy {
background: rgba(46, 204, 113, 0.15);
border-color: #2ecc71;
color: #2ecc71;
}
.badge-diff-medium {
background: rgba(243, 156, 18, 0.15);
border-color: #f39c12;
color: #f39c12;
}
.badge-diff-hard {
background: rgba(231, 76, 60, 0.15);
border-color: #e74c3c;
color: #e74c3c;
}
.badge-warning { .badge-warning {
background: rgba(243, 156, 18, 0.2); background: rgba(243, 156, 18, 0.2);
border-color: var(--warning); border-color: var(--warning);
@@ -1055,4 +1172,14 @@ body {
.admin-top-actions { .admin-top-actions {
justify-content: center; justify-content: center;
} }
.lang-btn {
padding: 4px 6px;
font-size: 0.75rem;
}
.diff-chip {
padding: 6px 10px;
font-size: 0.8rem;
}
} }
+166 -22
View File
@@ -1,9 +1,72 @@
let currentSessionId = null; let currentSessionId = null;
let currentQuestionNumber = 1; let currentQuestionNumber = 1;
let totalQuestions = 15; let totalQuestions = 10;
let currentScore = 0; let currentScore = 0;
let attemptsUsed = 0; let attemptsUsed = 0;
const maxAttempts = 3; const maxAttempts = 3;
let selectedDifficulty = "all";
let lastQuestionData = null;
const UI_TEXT = {
de: {
pageTitle: "✨ Magisches Hogwarts-Escape-Quiz ✨",
pageSubtitle: "10 knifflige Rätsel aus der Zauberwelt warten auf dich",
envelopeLetter: "Hogwarts-Schule für Hexerei und Zauberei",
introDesc: "Ein verzauberter Brief aus Hogwarts schwebt vor dir in der Luft.<br>Bist du bereit, die Prüfungen abzulegen und deine Hauspunkte zu sichern?",
diffLabel: "Schwierigkeitsgrad wählen:",
diffAll: "Alle gemischt",
diffEasy: "Leicht",
diffMed: "Mittel",
diffHard: "Schwer",
startBtn: "📜 Siegel brechen & Brief öffnen",
startBtnCasting: "✨ Zauber wird gewirkt...",
pointsLabel: "🏆 Hauspunkte",
questionLabel: "Frage",
ofLabel: "von",
attemptLabel: "Versuch",
checkBtn: "✨ Zauber prüfen",
hintBtn: "💡 Zaubertipp",
hintHeading: "📜 Notiz aus der verbotenen Abteilung:",
placeholder: "Deine Antwort hier eingeben...",
noHint: "Kein Tipp vorhanden.",
hintFailed: "Tipp konnte nicht geladen werden.",
correctFeedback: "✨ Richtig! 1 Hauspunkt für dein Haus! ✨",
wrongFeedback: "Leider falsch! Noch {attempts} Versuch(e).",
exhaustedWrong: "Leider nicht richtig (3 Fehlversuche).",
solutionLabel: "Richtige Lösung:",
startError: "Fehler beim Starten des Quiz: ",
networkError: "Netzwerkfehler: "
},
en: {
pageTitle: "✨ Magical Hogwarts Escape Quiz ✨",
pageSubtitle: "10 tricky wizarding world puzzles await you",
envelopeLetter: "Hogwarts School of Witchcraft and Wizardry",
introDesc: "An enchanted letter from Hogwarts is floating in the air before you.<br>Are you ready to take the exams and secure House Points?",
diffLabel: "Choose difficulty level:",
diffAll: "All mixed",
diffEasy: "Easy",
diffMed: "Medium",
diffHard: "Hard",
startBtn: "📜 Break Seal & Open Letter",
startBtnCasting: "✨ Casting spell...",
pointsLabel: "🏆 House Points",
questionLabel: "Question",
ofLabel: "of",
attemptLabel: "Attempt",
checkBtn: "✨ Check Spell",
hintBtn: "💡 Magic Hint",
hintHeading: "📜 Note from the Restricted Section:",
placeholder: "Type your answer here...",
noHint: "No hint available.",
hintFailed: "Could not load hint.",
correctFeedback: "✨ Correct! 1 House Point for your House! ✨",
wrongFeedback: "Incorrect! {attempts} attempt(s) remaining.",
exhaustedWrong: "Unfortunately incorrect (3 failed attempts).",
solutionLabel: "Correct solution:",
startError: "Error starting quiz: ",
networkError: "Network error: "
}
};
// Web Audio API Sound Generator for magic chimes and buzzers // Web Audio API Sound Generator for magic chimes and buzzers
const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
@@ -43,12 +106,63 @@ function playSound(type) {
} }
} }
function selectDifficulty(diff) {
selectedDifficulty = diff;
document.querySelectorAll(".diff-chip").forEach(chip => {
chip.classList.toggle("active", chip.getAttribute("data-diff") === diff);
});
}
function onLanguageChange(lang) {
applyQuizTranslations(lang);
}
function applyQuizTranslations(lang) {
const t = UI_TEXT[lang] || UI_TEXT.de;
const pageTitle = document.getElementById("pageTitle");
const pageSubtitle = document.getElementById("pageSubtitle");
const envelopeLetter = document.getElementById("envelopeLetter");
const introDesc = document.getElementById("introDesc");
const diffLabel = document.getElementById("diffLabel");
const diffAllText = document.getElementById("diffAllText");
const diffEasyText = document.getElementById("diffEasyText");
const diffMedText = document.getElementById("diffMedText");
const diffHardText = document.getElementById("diffHardText");
const startBtn = document.getElementById("startBtn");
const checkBtn = document.getElementById("checkBtn");
const hintBtn = document.getElementById("hintBtn");
const hintBtnMC = document.getElementById("hintBtnMC");
const hintHeading = document.getElementById("hintHeading");
const answerInput = document.getElementById("answerInput");
if (pageTitle) pageTitle.innerText = t.pageTitle;
if (pageSubtitle) pageSubtitle.innerText = t.pageSubtitle;
if (envelopeLetter) envelopeLetter.innerText = t.envelopeLetter;
if (introDesc) introDesc.innerHTML = t.introDesc;
if (diffLabel) diffLabel.innerText = t.diffLabel;
if (diffAllText) diffAllText.innerText = t.diffAll;
if (diffEasyText) diffEasyText.innerText = t.diffEasy;
if (diffMedText) diffMedText.innerText = t.diffMed;
if (diffHardText) diffHardText.innerText = t.diffHard;
if (startBtn && !startBtn.disabled) startBtn.innerText = t.startBtn;
if (checkBtn) checkBtn.innerText = t.checkBtn;
if (hintBtn) hintBtn.innerText = t.hintBtn;
if (hintBtnMC) hintBtnMC.innerText = t.hintBtn;
if (hintHeading) hintHeading.innerText = t.hintHeading;
if (answerInput) answerInput.placeholder = t.placeholder;
if (lastQuestionData) {
renderQuestion(lastQuestionData);
}
}
async function startGame() { async function startGame() {
const envelope = document.getElementById("envelope"); const envelope = document.getElementById("envelope");
const startBtn = document.getElementById("startBtn"); const startBtn = document.getElementById("startBtn");
const t = UI_TEXT[currentLang] || UI_TEXT.de;
startBtn.disabled = true; startBtn.disabled = true;
startBtn.innerText = "✨ Zauber wird gewirkt..."; startBtn.innerText = t.startBtnCasting;
envelope.classList.add("envelope-opened"); envelope.classList.add("envelope-opened");
// Start background music if enabled or first click // Start background music if enabled or first click
@@ -59,7 +173,7 @@ async function startGame() {
const text = document.getElementById("audioText"); const text = document.getElementById("audioText");
if (icon && text) { if (icon && text) {
icon.innerText = "🔊"; icon.innerText = "🔊";
text.innerText = "Musik: An"; text.innerText = (currentLang === "de") ? "Musik: An" : "Music: On";
} }
localStorage.setItem("bgm_enabled", "true"); localStorage.setItem("bgm_enabled", "true");
}).catch(() => {}); }).catch(() => {});
@@ -68,11 +182,15 @@ async function startGame() {
try { try {
const res = await fetch("/api/game/start", { const res = await fetch("/api/game/start", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" } headers: { "Content-Type": "application/json" },
body: JSON.stringify({
lang: currentLang,
difficulty: selectedDifficulty
})
}); });
if (!res.ok) { if (!res.ok) {
throw new Error("Konnte Spiel nicht starten."); throw new Error(currentLang === "en" ? "Could not start game session." : "Konnte Spiel nicht starten.");
} }
const data = await res.json(); const data = await res.json();
@@ -89,18 +207,39 @@ async function startGame() {
}, 1100); }, 1100);
} catch (err) { } catch (err) {
alert("Fehler beim Starten des Quiz: " + err.message); alert(t.startError + err.message);
startBtn.disabled = false; startBtn.disabled = false;
startBtn.innerText = "📜 Siegel brechen & Brief öffnen"; startBtn.innerText = t.startBtn;
envelope.classList.remove("envelope-opened"); envelope.classList.remove("envelope-opened");
} }
} }
function renderQuestion(qData) { function renderQuestion(qData) {
document.getElementById("questionCounter").innerText = `Frage ${qData.question_number} von ${totalQuestions}`; lastQuestionData = qData;
document.getElementById("pointsCounter").innerText = `🏆 Hauspunkte: ${currentScore}`; const t = UI_TEXT[currentLang] || UI_TEXT.de;
document.getElementById("questionCounter").innerText = `${t.questionLabel} ${qData.question_number} ${t.ofLabel} ${totalQuestions}`;
document.getElementById("pointsCounter").innerText = `${t.pointsLabel}: ${currentScore}`;
document.getElementById("badgeCategory").innerText = `⚡ ${qData.category.replace("_", " ").toUpperCase()}`; document.getElementById("badgeCategory").innerText = `⚡ ${qData.category.replace("_", " ").toUpperCase()}`;
document.getElementById("attemptBadge").innerText = `Versuch ${attemptsUsed + 1} von ${maxAttempts}`;
// Difficulty badge
const badgeDiff = document.getElementById("badgeDifficulty");
if (badgeDiff) {
const diff = qData.difficulty || "medium";
badgeDiff.className = `badge badge-diff badge-diff-${diff}`;
let diffText = t.diffMed;
let diffIcon = "🟡";
if (diff === "easy") {
diffText = t.diffEasy;
diffIcon = "🟢";
} else if (diff === "hard") {
diffText = t.diffHard;
diffIcon = "🔴";
}
badgeDiff.innerText = `${diffIcon} ${diffText}`;
}
document.getElementById("attemptBadge").innerText = `${t.attemptLabel} ${attemptsUsed + 1} ${t.ofLabel} ${maxAttempts}`;
document.getElementById("frageText").innerText = qData.question; document.getElementById("frageText").innerText = qData.question;
const textGroup = document.getElementById("textAnswerGroup"); const textGroup = document.getElementById("textAnswerGroup");
@@ -129,6 +268,7 @@ function renderQuestion(qData) {
choiceGroup.classList.add("hidden"); choiceGroup.classList.add("hidden");
textGroup.classList.remove("hidden"); textGroup.classList.remove("hidden");
input.value = ""; input.value = "";
input.placeholder = t.placeholder;
input.disabled = false; input.disabled = false;
document.getElementById("checkBtn").disabled = false; document.getElementById("checkBtn").disabled = false;
input.focus(); input.focus();
@@ -160,6 +300,8 @@ function checkAnswer() {
async function submitAnswer(answer, clickedBtn = null) { async function submitAnswer(answer, clickedBtn = null) {
const checkBtn = document.getElementById("checkBtn"); const checkBtn = document.getElementById("checkBtn");
const input = document.getElementById("answerInput"); const input = document.getElementById("answerInput");
const t = UI_TEXT[currentLang] || UI_TEXT.de;
if (checkBtn) checkBtn.disabled = true; if (checkBtn) checkBtn.disabled = true;
if (input) input.disabled = true; if (input) input.disabled = true;
@@ -181,12 +323,12 @@ async function submitAnswer(answer, clickedBtn = null) {
}); });
if (!res.ok) { if (!res.ok) {
throw new Error("Fehler bei der Antwortprüfung."); throw new Error(currentLang === "en" ? "Answer check failed." : "Fehler bei der Antwortprüfung.");
} }
const result = await res.json(); const result = await res.json();
currentScore = result.score; currentScore = result.score;
document.getElementById("pointsCounter").innerText = `🏆 Hauspunkte: ${currentScore}`; document.getElementById("pointsCounter").innerText = `${t.pointsLabel}: ${currentScore}`;
const frageBox = document.getElementById("frageBox"); const frageBox = document.getElementById("frageBox");
const feedbackBox = document.getElementById("feedbackBox"); const feedbackBox = document.getElementById("feedbackBox");
@@ -199,7 +341,7 @@ async function submitAnswer(answer, clickedBtn = null) {
clickedBtn.classList.add("choice-correct"); clickedBtn.classList.add("choice-correct");
} }
frageBox.classList.add("sparkle-effect"); frageBox.classList.add("sparkle-effect");
feedbackBox.innerHTML = `<span class="feedback-correct">✨ Richtig! 1 Hauspunkt für dein Haus! ✨</span>`; feedbackBox.innerHTML = `<span class="feedback-correct">${t.correctFeedback}</span>`;
setTimeout(() => { setTimeout(() => {
frageBox.classList.remove("sparkle-effect"); frageBox.classList.remove("sparkle-effect");
@@ -223,13 +365,13 @@ async function submitAnswer(answer, clickedBtn = null) {
setTimeout(() => frageBox.classList.remove("shake-effect"), 500); setTimeout(() => frageBox.classList.remove("shake-effect"), 500);
attemptsUsed = result.attempts_used; attemptsUsed = result.attempts_used;
document.getElementById("attemptBadge").innerText = `Versuch ${Math.min(attemptsUsed + 1, maxAttempts)} von ${maxAttempts}`; document.getElementById("attemptBadge").innerText = `${t.attemptLabel} ${Math.min(attemptsUsed + 1, maxAttempts)} ${t.ofLabel} ${maxAttempts}`;
if (result.attempts_left === 0) { if (result.attempts_left === 0) {
// Exhausted all attempts // Exhausted all attempts
feedbackBox.innerHTML = ` feedbackBox.innerHTML = `
<div class="feedback-wrong">Leider nicht richtig (3 Fehlversuche).</div> <div class="feedback-wrong">${t.exhaustedWrong}</div>
<div class="feedback-revealed">Richtige Lösung: <strong>${result.revealed_answer}</strong></div> <div class="feedback-revealed">${t.solutionLabel} <strong>${result.revealed_answer}</strong></div>
`; `;
setTimeout(() => { setTimeout(() => {
@@ -244,7 +386,8 @@ async function submitAnswer(answer, clickedBtn = null) {
} else { } else {
// Still attempts remaining // Still attempts remaining
feedbackBox.innerHTML = `<span class="feedback-wrong">Leider falsch! Noch ${result.attempts_left} Versuch(e).</span>`; const msg = t.wrongFeedback.replace("{attempts}", result.attempts_left);
feedbackBox.innerHTML = `<span class="feedback-wrong">${msg}</span>`;
if (checkBtn) checkBtn.disabled = false; if (checkBtn) checkBtn.disabled = false;
if (input) { if (input) {
input.disabled = false; input.disabled = false;
@@ -256,7 +399,7 @@ async function submitAnswer(answer, clickedBtn = null) {
} }
} catch (err) { } catch (err) {
alert("Netzwerkfehler: " + err.message); alert(t.networkError + err.message);
if (checkBtn) checkBtn.disabled = false; if (checkBtn) checkBtn.disabled = false;
if (input) input.disabled = false; if (input) input.disabled = false;
choiceBtns.forEach(b => b.disabled = false); choiceBtns.forEach(b => b.disabled = false);
@@ -267,22 +410,22 @@ async function requestHint() {
if (!currentSessionId) return; if (!currentSessionId) return;
const hintBox = document.getElementById("hintBox"); const hintBox = document.getElementById("hintBox");
const hintText = document.getElementById("hintText"); const hintText = document.getElementById("hintText");
const t = UI_TEXT[currentLang] || UI_TEXT.de;
try { try {
const res = await fetch(`/api/game/hint?session_id=${encodeURIComponent(currentSessionId)}`); const res = await fetch(`/api/game/hint?session_id=${encodeURIComponent(currentSessionId)}`);
const data = await res.json(); const data = await res.json();
hintText.innerText = data.hint || "Kein Tipp vorhanden."; hintText.innerText = data.hint || t.noHint;
hintBox.classList.remove("hidden"); hintBox.classList.remove("hidden");
} catch (err) { } catch (err) {
hintText.innerText = "Tipp konnte nicht geladen werden."; hintText.innerText = t.hintFailed;
hintBox.classList.remove("hidden"); hintBox.classList.remove("hidden");
} }
} }
function finishGame() { function finishGame() {
// 100% progress
document.getElementById("progressBar").style.width = "100%"; document.getElementById("progressBar").style.width = "100%";
window.location.href = `/finale?score=${currentScore}&total=${totalQuestions}`; window.location.href = `/finale?score=${currentScore}&total=${totalQuestions}&lang=${currentLang}`;
} }
// Enter key to submit // Enter key to submit
@@ -296,4 +439,5 @@ document.addEventListener("DOMContentLoaded", () => {
} }
}); });
} }
applyQuizTranslations(currentLang);
}); });
+119 -9
View File
@@ -1,32 +1,136 @@
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):",
namePlaceholder: "z. B. Hermine Granger",
frageLabel: "Deine Frage: *",
fragePlaceholder: "z. B. Wie heißt der Schulleiter von Hogwarts?",
diffLabel: "Schwierigkeitsgrad:",
typeLabel: "Art der Frage:",
optText: "🔤 Freitext-Antwort (Tippen mit Fehlertoleranz)",
optMC: "🔘 Multiple Choice (Auswahl aus 4 Antwortmöglichkeiten)",
antwortLabelText: "Richtige Antwort:",
antwortLabelMC: "Richtige Antwort (die korrekte Option):",
antwortHelpText: "Unser System gleicht Tippfehler und Groß-/Kleinschreibung automatisch ab.",
antwortHelpMC: "Diese Antwort wird zusammen mit den falschen Optionen zufällig für den Spieler angeordnet.",
wrongLabel: "Falsche Antwortmöglichkeiten (zur Auswahl für die Spieler):",
wrongPlaceholder1: "Falsche Option 1 (z. B. Severus Snape)",
wrongPlaceholder2: "Falsche Option 2 (z. B. Minerva McGonagall)",
wrongPlaceholder3: "Falsche Option 3 (z. B. Rubeus Hagrid)",
tippLabel: "Tipp / Hinweis (optional):",
tippPlaceholder: "z. B. Er trägt eine Halbmondbrille.",
submitBtn: "📜 Vorschlag an das Ministerium senden",
submitBtnCasting: "✨ Vorschlag wird übertragen...",
alertFillRequired: "Bitte gib mindestens die Frage und die richtige Antwort ein.",
alertMCRequired: "Bitte gib bei Multiple-Choice mindestens eine falsche Antwortmöglichkeit an.",
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):",
namePlaceholder: "e.g. Hermione Granger",
frageLabel: "Your Question: *",
fragePlaceholder: "e.g. Who is the Headmaster of Hogwarts?",
diffLabel: "Difficulty Level:",
typeLabel: "Question Type:",
optText: "🔤 Free-Text Input (with typo tolerance)",
optMC: "🔘 Multiple Choice (Selection of 4 options)",
antwortLabelText: "Correct Answer:",
antwortLabelMC: "Correct Answer (the winning option):",
antwortHelpText: "Our system automatically matches minor typos and casing.",
antwortHelpMC: "This option will be shuffled together with the false choices.",
wrongLabel: "Wrong Answer Options (for player to choose from):",
wrongPlaceholder1: "Wrong Option 1 (e.g. Severus Snape)",
wrongPlaceholder2: "Wrong Option 2 (e.g. Minerva McGonagall)",
wrongPlaceholder3: "Wrong Option 3 (e.g. Rubeus Hagrid)",
tippLabel: "Hint / Clue (optional):",
tippPlaceholder: "e.g. He wears half-moon spectacles.",
submitBtn: "📜 Submit Proposal to the Ministry",
submitBtnCasting: "✨ Transmitting proposal...",
alertFillRequired: "Please enter at least the question and the correct answer.",
alertMCRequired: "For Multiple Choice, please provide at least one incorrect option.",
successHeader: "✨ Question successfully submitted! ✨"
}
};
function onLanguageChange(lang) {
const t = SUGGESTION_TEXT[lang] || SUGGESTION_TEXT.de;
const title = document.getElementById("suggTitle");
const subtitle = document.getElementById("suggSubtitle");
const alertBox = document.getElementById("suggAlert");
const nameLabel = document.getElementById("suggNameLabel");
const nameInput = document.getElementById("name");
const frageLabel = document.getElementById("suggFrageLabel");
const frageInput = document.getElementById("frage");
const diffLabel = document.getElementById("suggDiffLabel");
const typeLabel = document.getElementById("suggTypeLabel");
const optText = document.getElementById("optText");
const optMC = document.getElementById("optMC");
const mcWrongLabel = document.getElementById("mcWrongLabel");
const tippLabel = document.getElementById("suggTippLabel");
const tippInput = document.getElementById("tipp");
const submitBtn = document.getElementById("submitBtn");
if (title) title.innerText = t.title;
if (subtitle) subtitle.innerText = t.subtitle;
if (alertBox) alertBox.innerHTML = t.alert;
if (nameLabel) nameLabel.innerText = t.nameLabel;
if (nameInput) nameInput.placeholder = t.namePlaceholder;
if (frageLabel) frageLabel.innerText = t.frageLabel;
if (frageInput) frageInput.placeholder = t.fragePlaceholder;
if (diffLabel) diffLabel.innerText = t.diffLabel;
if (typeLabel) typeLabel.innerText = t.typeLabel;
if (optText) optText.innerText = t.optText;
if (optMC) optMC.innerText = t.optMC;
if (mcWrongLabel) mcWrongLabel.innerText = t.wrongLabel;
if (tippLabel) tippLabel.innerText = t.tippLabel;
if (tippInput) tippInput.placeholder = t.tippPlaceholder;
if (submitBtn && !submitBtn.disabled) submitBtn.innerText = t.submitBtn;
const qType = document.getElementById("q_type");
if (qType) toggleVorschlagType(qType.value);
}
function toggleVorschlagType(type) { function toggleVorschlagType(type) {
const lang = (typeof currentLang !== "undefined") ? currentLang : (localStorage.getItem("quiz_lang") || "de");
const t = SUGGESTION_TEXT[lang] || SUGGESTION_TEXT.de;
const mcGroup = document.getElementById("mcOptionsGroup"); const mcGroup = document.getElementById("mcOptionsGroup");
const antwortLabel = document.getElementById("antwortLabel"); const antwortLabel = document.getElementById("antwortLabel");
const antwortHelp = document.getElementById("antwortHelp"); const antwortHelp = document.getElementById("antwortHelp");
if (type === "multiple_choice") { if (type === "multiple_choice") {
mcGroup.classList.remove("hidden"); mcGroup.classList.remove("hidden");
antwortLabel.innerText = "Richtige Antwort (die korrekte Option):"; antwortLabel.innerText = t.antwortLabelMC;
antwortHelp.innerText = "Diese Antwort wird zusammen mit den falschen Optionen zufällig für den Spieler angeordnet."; antwortHelp.innerText = t.antwortHelpMC;
} else { } else {
mcGroup.classList.add("hidden"); mcGroup.classList.add("hidden");
antwortLabel.innerText = "Richtige Antwort:"; antwortLabel.innerText = t.antwortLabelText;
antwortHelp.innerText = "Unser System gleicht Tippfehler und Groß-/Kleinschreibung automatisch ab."; antwortHelp.innerText = t.antwortHelpText;
} }
} }
async function submitSuggestion(event) { async function submitSuggestion(event) {
event.preventDefault(); event.preventDefault();
const lang = (typeof currentLang !== "undefined") ? currentLang : (localStorage.getItem("quiz_lang") || "de");
const t = SUGGESTION_TEXT[lang] || SUGGESTION_TEXT.de;
const name = document.getElementById("name").value.trim(); const name = document.getElementById("name").value.trim();
const frage = document.getElementById("frage").value.trim(); const frage = document.getElementById("frage").value.trim();
const antwort = document.getElementById("antwort").value.trim(); const antwort = document.getElementById("antwort").value.trim();
const tipp = document.getElementById("tipp").value.trim(); const tipp = document.getElementById("tipp").value.trim();
const qType = document.getElementById("q_type").value; const qType = document.getElementById("q_type").value;
const diff = document.getElementById("diff_select") ? document.getElementById("diff_select").value : "medium";
const submitBtn = document.getElementById("submitBtn"); const submitBtn = document.getElementById("submitBtn");
const resultBox = document.getElementById("resultMessage"); const resultBox = document.getElementById("resultMessage");
if (!frage || !antwort) { if (!frage || !antwort) {
alert("Bitte gib mindestens die Frage und die richtige Antwort ein."); alert(t.alertFillRequired);
return; return;
} }
@@ -38,13 +142,13 @@ async function submitSuggestion(event) {
falscheAntworten = [w1, w2, w3].filter(x => x.length > 0); falscheAntworten = [w1, w2, w3].filter(x => x.length > 0);
if (falscheAntworten.length === 0) { if (falscheAntworten.length === 0) {
alert("Bitte gib bei Multiple-Choice mindestens eine falsche Antwortmöglichkeit an."); alert(t.alertMCRequired);
return; return;
} }
} }
submitBtn.disabled = true; submitBtn.disabled = true;
submitBtn.innerText = "✨ Vorschlag wird übertragen..."; submitBtn.innerText = t.submitBtnCasting;
try { try {
const payload = { const payload = {
@@ -53,6 +157,7 @@ async function submitSuggestion(event) {
antwort: antwort, antwort: antwort,
tipp: tipp || null, tipp: tipp || null,
question_type: qType, question_type: qType,
difficulty: diff,
falsche_antworten: falscheAntworten falsche_antworten: falscheAntworten
}; };
@@ -67,7 +172,7 @@ async function submitSuggestion(event) {
if (res.ok && data.success) { if (res.ok && data.success) {
resultBox.innerHTML = ` resultBox.innerHTML = `
<div style="background: rgba(46, 204, 113, 0.2); border: 1px solid #2ecc71; color: #55efc4; padding: 15px; border-radius: 8px;"> <div style="background: rgba(46, 204, 113, 0.2); border: 1px solid #2ecc71; color: #55efc4; padding: 15px; border-radius: 8px;">
<h3>✨ Vorschlag erfolgreich übermittelt! ✨</h3> <h3>${t.successHeader}</h3>
<p>${data.message}</p> <p>${data.message}</p>
</div> </div>
`; `;
@@ -91,6 +196,11 @@ async function submitSuggestion(event) {
resultBox.classList.remove("hidden"); resultBox.classList.remove("hidden");
} finally { } finally {
submitBtn.disabled = false; submitBtn.disabled = false;
submitBtn.innerText = "📜 Vorschlag an das Ministerium senden"; submitBtn.innerText = t.submitBtn;
} }
} }
document.addEventListener("DOMContentLoaded", () => {
const lang = (typeof currentLang !== "undefined") ? currentLang : (localStorage.getItem("quiz_lang") || "de");
onLanguageChange(lang);
});
+19
View File
@@ -54,6 +54,7 @@
<div class="q-header"> <div class="q-header">
<div style="display: flex; gap: 8px; align-items: center;"> <div style="display: flex; gap: 8px; align-items: center;">
<span class="badge badge-warning">⚠️ Prüfung ausstehend</span> <span class="badge badge-warning">⚠️ Prüfung ausstehend</span>
<span class="badge badge-diff badge-diff-{{ q.difficulty or 'medium' }}">{{ (q.difficulty or 'medium')|capitalize }}</span>
{% if q.question_type == 'multiple_choice' %} {% if q.question_type == 'multiple_choice' %}
<span class="badge" style="background: rgba(155, 89, 182, 0.2); border-color: #9b59b6; color: #d2b4de;">🔘 Multiple-Choice</span> <span class="badge" style="background: rgba(155, 89, 182, 0.2); border-color: #9b59b6; color: #d2b4de;">🔘 Multiple-Choice</span>
{% else %} {% else %}
@@ -143,6 +144,7 @@
<div class="q-header"> <div class="q-header">
<div style="display: flex; gap: 8px; align-items: center;"> <div style="display: flex; gap: 8px; align-items: center;">
<span class="badge badge-success">Reihenfolge #{{ q.sort_order }}</span> <span class="badge badge-success">Reihenfolge #{{ q.sort_order }}</span>
<span class="badge badge-diff badge-diff-{{ q.difficulty or 'medium' }}">{{ (q.difficulty or 'medium')|capitalize }}</span>
{% if q.question_type == 'multiple_choice' %} {% if q.question_type == 'multiple_choice' %}
<span class="badge" style="background: rgba(155, 89, 182, 0.2); border-color: #9b59b6; color: #d2b4de;">🔘 Multiple-Choice</span> <span class="badge" style="background: rgba(155, 89, 182, 0.2); border-color: #9b59b6; color: #d2b4de;">🔘 Multiple-Choice</span>
{% else %} {% else %}
@@ -182,6 +184,14 @@
<option value="multiple_choice" {% if q.question_type == 'multiple_choice' %}selected{% endif %}>🔘 Multiple Choice</option> <option value="multiple_choice" {% if q.question_type == 'multiple_choice' %}selected{% endif %}>🔘 Multiple Choice</option>
</select> </select>
</div> </div>
<div class="form-group">
<label>Schwierigkeitsgrad:</label>
<select name="difficulty" class="input-text">
<option value="easy" {% if q.difficulty == 'easy' %}selected{% endif %}>🟢 Leicht</option>
<option value="medium" {% if q.difficulty == 'medium' or not q.difficulty %}selected{% endif %}>🟡 Mittel</option>
<option value="hard" {% if q.difficulty == 'hard' %}selected{% endif %}>🔴 Schwer</option>
</select>
</div>
<div class="form-group"> <div class="form-group">
<label>Richtige Antwort:</label> <label>Richtige Antwort:</label>
<input type="text" name="canonical_answer" class="input-text" value="{{ q.canonical_answer }}" required> <input type="text" name="canonical_answer" class="input-text" value="{{ q.canonical_answer }}" required>
@@ -221,6 +231,15 @@
</select> </select>
</div> </div>
<div class="form-group">
<label for="new_diff">Schwierigkeitsgrad:</label>
<select name="difficulty" id="new_diff" class="input-text">
<option value="easy">🟢 Leicht</option>
<option value="medium" selected>🟡 Mittel</option>
<option value="hard">🔴 Schwer</option>
</select>
</div>
<div class="form-group"> <div class="form-group">
<label for="new_type">Fragentyp:</label> <label for="new_type">Fragentyp:</label>
<select name="question_type" id="new_type" class="input-text" onchange="toggleAdminNewType(this.value)"> <select name="question_type" id="new_type" class="input-text" onchange="toggleAdminNewType(this.value)">
+62 -7
View File
@@ -19,6 +19,11 @@
</a> </a>
<div class="header-right"> <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()"> <button id="audioToggle" class="audio-btn" title="Musik an/aus" onclick="toggleAudio()">
<span id="audioIcon">🔇</span> <span id="audioIcon">🔇</span>
<span id="audioText" class="audio-label">Musik: Aus</span> <span id="audioText" class="audio-label">Musik: Aus</span>
@@ -30,9 +35,9 @@
</div> </div>
<nav id="navLinks" class="nav-links"> <nav id="navLinks" class="nav-links">
<a href="/" class="nav-link" onclick="closeMobileMenu()">✨ Zum Quiz</a> <a href="/" class="nav-link" id="navQuiz" onclick="closeMobileMenu()">✨ Zum Quiz</a>
<a href="/vorschlag" class="nav-link" onclick="closeMobileMenu()">✍️ Frage einreichen</a> <a href="/vorschlag" class="nav-link" id="navSuggest" onclick="closeMobileMenu()">✍️ Frage einreichen</a>
<a href="/admin" class="nav-link" onclick="closeMobileMenu()">🔒 Admin-Cockpit</a> <a href="/admin" class="nav-link" id="navAdmin" onclick="closeMobileMenu()">🔒 Admin-Cockpit</a>
</nav> </nav>
</div> </div>
</header> </header>
@@ -42,7 +47,7 @@
</main> </main>
<footer class="footer"> <footer class="footer">
<p>⚡ Magisches Hogwarts-Escape-Quiz · Betrieben mit Liebe & Zauberkraft ⚡</p> <p id="footerText">⚡ Magisches Hogwarts-Escape-Quiz · Betrieben mit Liebe & Zauberkraft ⚡</p>
</footer> </footer>
<script> <script>
@@ -52,11 +57,56 @@
const navLinks = document.getElementById("navLinks"); const navLinks = document.getElementById("navLinks");
const menuIcon = document.getElementById("menuIcon"); 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 navSuggest = document.getElementById("navSuggest");
const navAdmin = document.getElementById("navAdmin");
const footerText = document.getElementById("footerText");
if (navQuiz) navQuiz.innerText = isDe ? "✨ Zum Quiz" : "✨ To Quiz";
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 (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() { function toggleAudio() {
const isDe = currentLang === "de";
if (bgm.paused) { if (bgm.paused) {
bgm.play().then(() => { bgm.play().then(() => {
audioIcon.innerText = "🔊"; audioIcon.innerText = "🔊";
audioText.innerText = "Musik: An"; audioText.innerText = isDe ? "Musik: An" : "Music: On";
localStorage.setItem("bgm_enabled", "true"); localStorage.setItem("bgm_enabled", "true");
}).catch(err => { }).catch(err => {
console.log("Audio play blocked:", err); console.log("Audio play blocked:", err);
@@ -64,7 +114,7 @@
} else { } else {
bgm.pause(); bgm.pause();
audioIcon.innerText = "🔇"; audioIcon.innerText = "🔇";
audioText.innerText = "Musik: Aus"; audioText.innerText = isDe ? "Musik: Aus" : "Music: Off";
localStorage.setItem("bgm_enabled", "false"); localStorage.setItem("bgm_enabled", "false");
} }
} }
@@ -84,11 +134,16 @@
if (localStorage.getItem("bgm_enabled") === "true" && bgm.paused) { if (localStorage.getItem("bgm_enabled") === "true" && bgm.paused) {
bgm.play().then(() => { bgm.play().then(() => {
audioIcon.innerText = "🔊"; audioIcon.innerText = "🔊";
audioText.innerText = "Musik: An"; audioText.innerText = (currentLang === "de") ? "Musik: An" : "Music: On";
}).catch(() => {}); }).catch(() => {});
} }
document.removeEventListener("click", initAudioOnInteraction); document.removeEventListener("click", initAudioOnInteraction);
}, { once: true }); }, { once: true });
// Initialize UI on load
document.addEventListener("DOMContentLoaded", () => {
updateLanguageUI();
});
</script> </script>
{% block scripts %}{% endblock %} {% block scripts %}{% endblock %}
</body> </body>
+79 -8
View File
@@ -1,35 +1,106 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}🎉 Das große Finale – Magisches Quiz{% endblock %} {% block title %}{% if lang == 'en' %}🎉 The Grand Finale – Magical Quiz{% else %}🎉 Das große Finale – Magisches Quiz{% endif %}{% endblock %}
{% block content %} {% block content %}
<div class="quiz-wrapper"> <div class="quiz-wrapper">
<div class="card finale-card"> <div class="card finale-card">
<div class="finale-header"> <div class="finale-header">
<span class="sparkle-icon">✨</span> <span class="sparkle-icon">✨</span>
<h1 class="glow-title">🎉 Happy Birthday Schatz! 🎉</h1> <h1 class="glow-title" id="finaleTitle">🎉 Happy Birthday Schatz! 🎉</h1>
<span class="sparkle-icon">✨</span> <span class="sparkle-icon">✨</span>
</div> </div>
<div class="score-banner"> <div class="score-banner">
<p class="score-highlight"> <p class="score-highlight" id="finaleScoreText">
{% if lang == 'en' %}
You secured <span class="gold-text">{{ score }}</span> out of <span class="gold-text">{{ total }}</span> possible House Points for your House!
{% else %}
Du hast <span class="gold-text">{{ score }}</span> von <span class="gold-text">{{ total }}</span> möglichen Hauspunkten für dein Haus gesichert! Du hast <span class="gold-text">{{ score }}</span> von <span class="gold-text">{{ total }}</span> möglichen Hauspunkten für dein Haus gesichert!
{% endif %}
</p> </p>
</div> </div>
<div class="reward-box"> <div class="reward-box">
<h2>💛 Dein magischer Gewinn: Ein unvergesslicher Abend Zuhause 💛</h2> <h2 id="rewardTitle">
<p class="reward-description"> {% if lang == 'en' %}
💛 Your Magical Reward: An Unforgettable Evening at Home 💛
{% else %}
💛 Dein magischer Gewinn: Ein unvergesslicher Abend Zuhause 💛
{% endif %}
</h2>
<p class="reward-description" id="rewardDesc">
{% if lang == 'en' %}
We'll order delicious food and make ourselves totally comfortable: relax, laugh, and enjoy quality time together.<br><br>
Plus, we'll play a brand new <strong>Online Escape Game</strong> of your choice!
{% else %}
Wir holen uns lecker Essen und machen es uns so richtig gemütlich: entspannen, lachen und die gemeinsame Zeit genießen.<br><br> 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! Dazu spielen wir ein brandneues <strong>Online Escape Game</strong> deiner Wahl!
{% endif %}
</p>
<p class="love-note" id="loveNote">
{% if lang == 'en' %}
I'm so excited for our wonderful evening together! 💫
{% else %}
Ich freue mich riesig auf unseren gemeinsamen Abend! 💫
{% endif %}
</p> </p>
<p class="love-note">Ich freue mich riesig auf unseren gemeinsamen Abend! 💫</p>
</div> </div>
<div class="btn-group finale-actions"> <div class="btn-group finale-actions">
<a href="/" class="btn btn-primary">🔄 Erneut spielen</a> <a href="/" class="btn btn-primary" id="playAgainBtn">
<a href="/vorschlag" class="btn btn-secondary">✍️ Eigene Frage vorschlagen</a> {% if lang == 'en' %}🔄 Play Again{% else %}🔄 Erneut spielen{% endif %}
</a>
<a href="/vorschlag" class="btn btn-secondary" id="suggestBtn">
{% if lang == 'en' %}✍️ Suggest Your Own Question{% else %}✍️ Eigene Frage vorschlagen{% endif %}
</a>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block scripts %}
<script>
const finaleScore = {{ score }};
const finaleTotal = {{ total }};
function onLanguageChange(lang) {
const isDe = lang === "de";
const title = document.getElementById("finaleTitle");
const scoreText = document.getElementById("finaleScoreText");
const rewardTitle = document.getElementById("rewardTitle");
const rewardDesc = document.getElementById("rewardDesc");
const loveNote = document.getElementById("loveNote");
const playAgain = document.getElementById("playAgainBtn");
const suggest = document.getElementById("suggestBtn");
if (title) title.innerText = isDe ? "🎉 Happy Birthday Schatz! 🎉" : "🎉 Happy Birthday Darling! 🎉";
if (scoreText) {
scoreText.innerHTML = isDe
? `Du hast <span class="gold-text">${finaleScore}</span> von <span class="gold-text">${finaleTotal}</span> möglichen Hauspunkten für dein Haus gesichert!`
: `You secured <span class="gold-text">${finaleScore}</span> out of <span class="gold-text">${finaleTotal}</span> possible House Points for your House!`;
}
if (rewardTitle) {
rewardTitle.innerText = isDe
? "💛 Dein magischer Gewinn: Ein unvergesslicher Abend Zuhause 💛"
: "💛 Your Magical Reward: An Unforgettable Evening at Home 💛";
}
if (rewardDesc) {
rewardDesc.innerHTML = isDe
? "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!"
: "We'll order delicious food and make ourselves totally comfortable: relax, laugh, and enjoy quality time together.<br><br>Plus, we'll play a brand new <strong>Online Escape Game</strong> of your choice!";
}
if (loveNote) {
loveNote.innerText = isDe
? "Ich freue mich riesig auf unseren gemeinsamen Abend! 💫"
: "I'm so excited for our wonderful evening together! 💫";
}
if (playAgain) playAgain.innerText = isDe ? "🔄 Erneut spielen" : "🔄 Play Again";
if (suggest) suggest.innerText = isDe ? "✍️ Eigene Frage vorschlagen" : "✍️ Suggest Your Own Question";
}
document.addEventListener("DOMContentLoaded", () => {
onLanguageChange(currentLang);
});
</script>
{% endblock %}
+30 -7
View File
@@ -5,8 +5,8 @@
{% block content %} {% block content %}
<div class="quiz-wrapper"> <div class="quiz-wrapper">
<header class="quiz-header"> <header class="quiz-header">
<h1 class="glow-title">✨ Magisches Hogwarts-Escape-Quiz ✨</h1> <h1 class="glow-title" id="pageTitle">✨ Magisches Hogwarts-Escape-Quiz ✨</h1>
<p class="subtitle">15 knifflige Rätsel aus der Zauberwelt warten auf dich</p> <p class="subtitle" id="pageSubtitle">10 knifflige Rätsel aus der Zauberwelt warten auf dich</p>
</header> </header>
<!-- Brief-Intro / Startmaske --> <!-- Brief-Intro / Startmaske -->
@@ -16,15 +16,35 @@
<div class="envelope-flap"></div> <div class="envelope-flap"></div>
<div class="wax-seal">H</div> <div class="wax-seal">H</div>
<div class="letter-preview"> <div class="letter-preview">
<p class="letter-text">Hogwarts-Schule für Hexerei und Zauberei</p> <p class="letter-text" id="envelopeLetter">Hogwarts-Schule für Hexerei und Zauberei</p>
</div> </div>
</div> </div>
</div> </div>
<div class="intro-content"> <div class="intro-content">
<p class="intro-text"> <p class="intro-text" id="introDesc">
Ein verzauberter Brief aus Hogwarts schwebt vor dir in der Luft.<br> Ein verzauberter Brief aus Hogwarts schwebt vor dir in der Luft.<br>
Bist du bereit, die Prüfungen abzulegen und deine Hauspunkte zu sichern? Bist du bereit, die Prüfungen abzulegen und deine Hauspunkte zu sichern?
</p> </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" onclick="startGame()"> <button id="startBtn" class="btn btn-primary btn-large" onclick="startGame()">
📜 Siegel brechen & Brief öffnen 📜 Siegel brechen & Brief öffnen
</button> </button>
@@ -36,7 +56,7 @@
<!-- Fortschritt & Punkte --> <!-- Fortschritt & Punkte -->
<div class="status-panel"> <div class="status-panel">
<div class="progress-info"> <div class="progress-info">
<span id="questionCounter">Frage 1 von {{ question_count }}</span> <span id="questionCounter">Frage 1 von 10</span>
<span id="pointsCounter" class="points-badge">🏆 Hauspunkte: 0</span> <span id="pointsCounter" class="points-badge">🏆 Hauspunkte: 0</span>
</div> </div>
<div class="progress-container"> <div class="progress-container">
@@ -47,7 +67,10 @@
<!-- Frage-Container --> <!-- Frage-Container -->
<div id="frageBox" class="card question-card"> <div id="frageBox" class="card question-card">
<div class="question-meta"> <div class="question-meta">
<span id="badgeCategory" class="badge">⚡ Harry Potter</span> <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> <span id="attemptBadge" class="attempt-badge">Versuch 1 von 3</span>
</div> </div>
@@ -78,7 +101,7 @@
</div> </div>
<div id="hintBox" class="hint-box hidden"> <div id="hintBox" class="hint-box hidden">
<strong>📜 Notiz aus der verbotenen Abteilung:</strong> <strong id="hintHeading">📜 Notiz aus der verbotenen Abteilung:</strong>
<p id="hintText"></p> <p id="hintText"></p>
</div> </div>
+19 -10
View File
@@ -5,29 +5,38 @@
{% block content %} {% block content %}
<div class="quiz-wrapper"> <div class="quiz-wrapper">
<div class="card vorschlag-card"> <div class="card vorschlag-card">
<h1 class="glow-title">✨ Neue Frage für das Quiz vorschlagen ✨</h1> <h1 class="glow-title" id="suggTitle">✨ Neue Frage für das Quiz vorschlagen ✨</h1>
<p class="subtitle">Bereichere das magische Quiz mit kniffligen Harry-Potter- oder Escape-Rätseln!</p> <p class="subtitle" id="suggSubtitle">Bereichere das magische Quiz mit kniffligen Harry-Potter- oder Escape-Rätseln!</p>
<div class="info-alert"> <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 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> </div>
<form id="suggestionForm" class="magic-form" onsubmit="submitSuggestion(event)"> <form id="suggestionForm" class="magic-form" onsubmit="submitSuggestion(event)">
<div class="form-group"> <div class="form-group">
<label for="name">Dein Name / Zauberer-Alias (optional):</label> <label for="name" id="suggNameLabel">Dein Name / Zauberer-Alias (optional):</label>
<input type="text" id="name" name="name" class="input-text" placeholder="z. B. Hermine Granger"> <input type="text" id="name" name="name" class="input-text" placeholder="z. B. Hermine Granger">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="frage">Deine Frage: <span class="required">*</span></label> <label for="frage" id="suggFrageLabel">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> <textarea id="frage" name="frage" class="textarea" rows="3" placeholder="z. B. Wie heißt der Schulleiter von Hogwarts?" required></textarea>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="q_type">Art der Frage:</label> <label for="diff_select" id="suggDiffLabel">Schwierigkeitsgrad:</label>
<select id="diff_select" name="difficulty" class="input-text">
<option value="easy">🟢 Leicht / Easy</option>
<option value="medium" selected>🟡 Mittel / Medium</option>
<option value="hard">🔴 Schwer / Hard</option>
</select>
</div>
<div class="form-group">
<label for="q_type" id="suggTypeLabel">Art der Frage:</label>
<select id="q_type" name="q_type" class="input-text" onchange="toggleVorschlagType(this.value)"> <select id="q_type" name="q_type" class="input-text" onchange="toggleVorschlagType(this.value)">
<option value="text" selected>🔤 Freitext-Antwort (Tippen mit Fehlertoleranz)</option> <option value="text" selected id="optText">🔤 Freitext-Antwort (Tippen mit Fehlertoleranz)</option>
<option value="multiple_choice">🔘 Multiple Choice (Auswahl aus 4 Antwortmöglichkeiten)</option> <option value="multiple_choice" id="optMC">🔘 Multiple Choice (Auswahl aus 4 Antwortmöglichkeiten)</option>
</select> </select>
</div> </div>
@@ -39,14 +48,14 @@
<!-- Zusätzliche Optionen für Multiple Choice --> <!-- Zusätzliche Optionen für Multiple Choice -->
<div id="mcOptionsGroup" class="form-group hidden" style="background: rgba(255,255,255,0.03); padding: 15px; border-radius: 8px; border: 1px dashed var(--border-gold);"> <div id="mcOptionsGroup" class="form-group hidden" style="background: rgba(255,255,255,0.03); padding: 15px; border-radius: 8px; border: 1px dashed var(--border-gold);">
<label style="color: var(--gold);">Falsche Antwortmöglichkeiten (zur Auswahl für die Spieler):</label> <label id="mcWrongLabel" style="color: var(--gold);">Falsche Antwortmöglichkeiten (zur Auswahl für die Spieler):</label>
<input type="text" id="wrong1" class="input-text" placeholder="Falsche Option 1 (z. B. Severus Snape)" style="margin-bottom: 8px;"> <input type="text" id="wrong1" class="input-text" placeholder="Falsche Option 1 (z. B. Severus Snape)" style="margin-bottom: 8px;">
<input type="text" id="wrong2" class="input-text" placeholder="Falsche Option 2 (z. B. Minerva McGonagall)" style="margin-bottom: 8px;"> <input type="text" id="wrong2" class="input-text" placeholder="Falsche Option 2 (z. B. Minerva McGonagall)" style="margin-bottom: 8px;">
<input type="text" id="wrong3" class="input-text" placeholder="Falsche Option 3 (z. B. Rubeus Hagrid)"> <input type="text" id="wrong3" class="input-text" placeholder="Falsche Option 3 (z. B. Rubeus Hagrid)">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="tipp">Tipp / Hinweis (optional):</label> <label for="tipp" id="suggTippLabel">Tipp / Hinweis (optional):</label>
<input type="text" id="tipp" name="tipp" class="input-text" placeholder="z. B. Er trägt eine Halbmondbrille."> <input type="text" id="tipp" name="tipp" class="input-text" placeholder="z. B. Er trägt eine Halbmondbrille.">
</div> </div>
+20
View File
@@ -0,0 +1,20 @@
from app.database import engine
from sqlalchemy import text
def migrate():
with engine.connect() as conn:
# Questions table
conn.execute(text("ALTER TABLE questions ADD COLUMN IF NOT EXISTS difficulty VARCHAR(20) DEFAULT 'medium'"))
conn.execute(text("ALTER TABLE questions ADD COLUMN IF NOT EXISTS question_en TEXT NULL"))
conn.execute(text("ALTER TABLE questions ADD COLUMN IF NOT EXISTS answer_en VARCHAR(255) NULL"))
conn.execute(text("ALTER TABLE questions ADD COLUMN IF NOT EXISTS options_en TEXT NULL"))
conn.execute(text("ALTER TABLE questions ADD COLUMN IF NOT EXISTS hint_en TEXT NULL"))
# Quiz sessions table
conn.execute(text("ALTER TABLE quiz_sessions ADD COLUMN IF NOT EXISTS lang VARCHAR(10) DEFAULT 'de'"))
conn.execute(text("ALTER TABLE quiz_sessions ADD COLUMN IF NOT EXISTS difficulty VARCHAR(20) DEFAULT 'all'"))
conn.commit()
print("Migration v3 successful: added difficulty, multilingual columns, and session preferences.")
if __name__ == "__main__":
migrate()