Feature: 40 bilingual questions, random 10 game run, difficulty levels, and full i18n support
This commit is contained in:
+87
-26
@@ -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
@@ -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
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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()
|
||||||
@@ -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
@@ -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
@@ -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);
|
||||||
|
});
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user