From 6c62676d0d601d1884ed7f989c8a3b39dacb8950 Mon Sep 17 00:00:00 2001 From: kobold-ai Date: Thu, 29 Oct 2026 08:51:28 +0100 Subject: [PATCH] feat: Add lore pack, UI adjustments, requirements.txt and INSTALL.md --- INSTALL.md | 89 + app/main.py | 31 +- app/matcher.py | 18 + app/schemas.py | 1 + app/seed.py | 4 + app/seed_lore_pack.py | 722 ++++++++ app/static/css/style.css | 3230 +++++++++++++++------------------- app/static/js/quiz.js | 1192 +++++++------ app/static/js/vorschlag.js | 42 +- app/templates/admin.html | 12 +- app/templates/base.html | 284 ++- app/templates/index.html | 334 ++-- app/templates/vorschlag.html | 18 +- requirements.txt | 29 + 14 files changed, 3281 insertions(+), 2725 deletions(-) create mode 100644 INSTALL.md create mode 100644 app/seed_lore_pack.py create mode 100644 requirements.txt diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..c0f52ae --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,89 @@ +# Magisches Harry Potter Escape Quiz Portal + +Interaktives Harry Potter Escape Quiz Portal (FastAPI & MariaDB). + +## Systemanforderungen +- Debian 12 / 13 (oder kompatibles Linux) +- Python 3.13+ mit `python3-venv` +- Nginx (Reverse Proxy mit SSL) +- MariaDB Server auf \`db.waiswuaschd.de:3306\` (Datenbank: \`v2_db_quiz\`) + +## 1. Installation +```bash +# Repository klonen +cd /home/maxi/Projekt +git clone https://gitea.sp13l.de/kobold-ai/quiz.git quiz.waiswuaschd.de +cd quiz.waiswuaschd.de + +# Python Virtualenv erstellen und Abh?ngigkeiten installieren +python3 -m venv venv +./venv/bin/pip install --upgrade pip +./venv/bin/pip install -r requirements.txt +``` + +## 2. Umgebungsvariablen (.env) +Erstelle im Projektverzeichnis eine `.env` Datei: +```ini +DB_HOST=db.waiswuaschd.de +DB_PORT=3306 +DB_NAME=v2_db_quiz +DB_USER=v2_usr_quiz +DB_PASS=Quiz_Pass_2026_Secure! +SECRET_KEY=hogwarts_magic_super_secret_key_2026_dev_quiz +ADMIN_USER=admin +ADMIN_PASS=Alohomora2026! +PORT=8001 +``` + +## 3. Systemd Service (/etc/systemd/system/quiz.service) +```ini +[Unit] +Description=Magisches Quiz Portal (FastAPI) +After=network.target + +[Service] +User=maxi +Group=maxi +WorkingDirectory=/home/maxi/Projekt/quiz.waiswuaschd.de +ExecStart=/home/maxi/Projekt/quiz.waiswuaschd.de/venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 8001 --workers 1 +Restart=always +RestartSec=5 +Environment=PYTHONUNBUFFERED=1 + +[Install] +WantedBy=multi-user.target +``` +Aktivieren und starten: +```bash +sudo systemctl daemon-reload +sudo systemctl enable --now quiz +``` + +## 4. Nginx Reverse Proxy +```nginx +server { + listen 80; + server_name quiz.dev.waiswuaschd.de; + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl http2; + server_name quiz.dev.waiswuaschd.de; + + # SSL Zertifikate hier konfigurieren (z. B. via certbot) + + location /static/ { + alias /home/maxi/Projekt/quiz.waiswuaschd.de/app/static/; + add_header Cache-Control "public, max-age=3600, must-revalidate"; + } + + location / { + proxy_pass http://127.0.0.1:8001; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +``` diff --git a/app/main.py b/app/main.py index 1623fe2..b251c38 100755 --- a/app/main.py +++ b/app/main.py @@ -20,7 +20,7 @@ from app.schemas import GameCheckRequest, SuggestionCreateRequest, GameStartRequ load_dotenv() -app = FastAPI(title="Magisches Harry-Potter-Escape-Quiz") +app = FastAPI(title="PotterTrivia – Das Harry-Potter-Wissensquiz") SECRET_KEY = os.getenv("SECRET_KEY", "hogwarts_secret_default_key_2026") app.add_middleware(SessionMiddleware, secret_key=SECRET_KEY, session_cookie="quiz_session") @@ -154,16 +154,37 @@ async def index(request: Request, db: Session = Depends(get_db)): context={"question_count": active_count} ) +VALID_CATEGORIES = ["all", "books", "movies", "creatures", "spells", "characters", "hogwarts", "quidditch", "lore", "general"] + @app.post("/api/game/start") async def start_game(payload: GameStartRequest = None, db: Session = Depends(get_db)): lang = payload.lang if payload and payload.lang in ["de", "en"] else "de" difficulty = payload.difficulty if payload and payload.difficulty in ["easy", "medium", "hard"] else "all" + category = payload.category if payload and payload.category in VALID_CATEGORIES else "all" + + base_query = db.query(Question).filter(Question.status == "active") + if category != "all": + if category == "books": + cat_query = base_query.filter((Question.category == "books") | (Question.question.like("%Kröter%"))) + else: + cat_query = base_query.filter(Question.category == category) + else: + cat_query = base_query - query = db.query(Question).filter(Question.status == "active") if difficulty != "all": - query = query.filter(Question.difficulty == difficulty) + strict_pool = cat_query.filter(Question.difficulty == difficulty).all() + if len(strict_pool) >= 10 or category == "all": + pool = strict_pool + else: + # Keep all difficulty matches in this category and top up from the same category + cat_all = cat_query.all() + strict_ids = {q.id for q in strict_pool} + extra = [q for q in cat_all if q.id not in strict_ids] + random.shuffle(extra) + pool = strict_pool + extra[:max(0, 10 - len(strict_pool))] + else: + pool = cat_query.all() - pool = query.all() if not pool: pool = db.query(Question).filter(Question.status == "active").all() if not pool: @@ -538,7 +559,7 @@ async def submit_suggestion(payload: SuggestionCreateRequest, db: Session = Depe opts.append(opt.strip()) options_json = json.dumps(opts, ensure_ascii=False) - chosen_cat = payload.category if payload.category in ["hogwarts", "characters", "spells", "creatures", "quidditch", "lore", "general"] else "general" + chosen_cat = payload.category if payload.category in ["books", "movies", "hogwarts", "characters", "spells", "creatures", "quidditch", "lore", "general"] else "general" diff = payload.difficulty if payload.difficulty in ["easy", "medium", "hard"] else "medium" new_suggestion = Question( diff --git a/app/matcher.py b/app/matcher.py index a751234..a55bfaa 100755 --- a/app/matcher.py +++ b/app/matcher.py @@ -115,4 +115,22 @@ def is_answer_correct(user_input: str, canonical: str, accepted_raw: str | None if dist <= max_allowed: return True + # Support compound answers connected by 'und' / 'and' (e.g. "Mantikor und Feuerkrabbe") + for sep in (" und ", " and "): + if sep in target: + parts = [p.strip() for p in target.split(sep) if len(p.strip()) >= 3] + if len(parts) == 2: + u_words = u.split() + matched_both = True + for part in parts: + if part in u: + continue + # Check fuzzy match against tokens or reversed order + part_matched = any(levenshtein_distance(w, part) <= (1 if len(part) <= 6 else 2) for w in u_words) + if not part_matched: + matched_both = False + break + if matched_both: + return True + return False diff --git a/app/schemas.py b/app/schemas.py index 94fd538..c8e9cf6 100755 --- a/app/schemas.py +++ b/app/schemas.py @@ -4,6 +4,7 @@ from typing import Optional, List class GameStartRequest(BaseModel): lang: Optional[str] = "de" difficulty: Optional[str] = "all" + category: Optional[str] = "all" recent_ids: Optional[List[int]] = None wrong_ids: Optional[List[int]] = None diff --git a/app/seed.py b/app/seed.py index 371dce2..45c97e5 100755 --- a/app/seed.py +++ b/app/seed.py @@ -4,6 +4,7 @@ from app.database import engine, SessionLocal, Base from app.models import Question, AdminUser from app.auth import hash_password from app.seed_40 import seed_40_questions +from app.seed_lore_pack import seed_lore_pack def seed_db(): Base.metadata.create_all(bind=engine) @@ -29,6 +30,9 @@ def seed_db(): print("Question table is empty, seeding 40 initial bilingual questions...") seed_40_questions() + # 3. Ensure Lore Pack (Books, Movies, Hard Creatures incl. Blast-Ended Skrewts) is present + seed_lore_pack() + finally: db.close() diff --git a/app/seed_lore_pack.py b/app/seed_lore_pack.py new file mode 100644 index 0000000..03f65ee --- /dev/null +++ b/app/seed_lore_pack.py @@ -0,0 +1,722 @@ +import json +from app.database import SessionLocal +from app.models import Question + +LORE_PACK_QUESTIONS = [ + # --- CREATURES (Magische Geschöpfe) --- + { + "category": "creatures", + "difficulty": "hard", + "question_type": "text", + "question": "Aus welcher Kreuzung zweier magischer Geschöpfe züchtete Hagrid die Knallrümpfigen Kröter?", + "canonical_answer": "Mantikor und Feuerkrabbe", + "accepted_answers": json.dumps([ + "Mantikor und Feuerkrabbe", + "Feuerkrabbe und Mantikor", + "Mantikor Feuerkrabbe", + "Feuerkrabbe Mantikor", + "Manticore and Fire Crab", + "Fire Crab and Manticore" + ], ensure_ascii=False), + "hint": "Eines ist ein hochgefährliches griechisches Ungeheuer mit Skorpionschwanz, das andere ein schildkrötenähnliches Wesen von den Fidschi-Inseln.", + "question_en": "Which two magical creatures did Hagrid crossbreed to create the Blast-Ended Skrewts?", + "answer_en": "Manticore and Fire Crab", + "hint_en": "One is a deadly Greek beast with a scorpion tail, the other is a turtle-like creature native to Fiji." + }, + { + "category": "creatures", + "difficulty": "hard", + "question_type": "multiple_choice", + "question": "Welcher der drei Köpfe der dreiköpfigen Schlange 'Runespoor' ist laut Newt Scamander derjenige, der Pläne schmiedet?", + "canonical_answer": "Der linke Kopf", + "options": json.dumps(["Der linke Kopf", "Der mittlere Kopf", "Der rechte Kopf", "Alle drei gleichzeitig"], ensure_ascii=False), + "hint": "Der mittlere Kopf ist der Träumer und der rechte Kopf der Kritiker.", + "question_en": "According to Newt Scamander, which of the three heads of the Runespoor snake is the planner?", + "answer_en": "The left head", + "options_en": json.dumps(["The left head", "The middle head", "The right head", "All three simultaneously"], ensure_ascii=False), + "hint_en": "The middle head is the dreamer and the right head is the critic." + }, + { + "category": "creatures", + "difficulty": "medium", + "question_type": "text", + "question": "Wie heißt die riesige Spinne (Acromantula), die Hagrid schon während seiner Schulzeit in einem Schrank in Hogwarts versteckte?", + "canonical_answer": "Aragog", + "accepted_answers": json.dumps(["Aragog"], ensure_ascii=False), + "hint": "Sein Name beginnt mit 'A' und er lebt später mit seiner großen Kolonie im Verbotenen Wald.", + "question_en": "What is the name of the giant Acromantula spider that Hagrid hid in a cupboard at Hogwarts during his school days?", + "answer_en": "Aragog", + "hint_en": "His name starts with 'A' and he later lives with his large colony in the Forbidden Forest." + }, + { + "category": "creatures", + "difficulty": "hard", + "question_type": "multiple_choice", + "question": "Welche Gefahrenklasse (Ministeriumsklassifizierung) hat ein ausgewachsener Nundu laut 'Phantastische Tierwesen'?", + "canonical_answer": "XXXXX (Bekannte Zauberertöter)", + "options": json.dumps([ + "XXXXX (Bekannte Zauberertöter)", + "XXXX (Gefährlich / erfordert Fachwissen)", + "XXX (Für fähige Zauberer kein Problem)", + "XX (Harmlos / kann gezähmt werden)" + ], ensure_ascii=False), + "hint": "Es braucht mindestens hundert zusammenarbeitende Zauberer, um einen einzigen Nundu zu überwältigen.", + "question_en": "What Ministry of Magic classification does an adult Nundu hold according to 'Fantastic Beasts'?", + "answer_en": "XXXXX (Known wizard killer)", + "options_en": json.dumps([ + "XXXXX (Known wizard killer)", + "XXXX (Dangerous / requires specialist knowledge)", + "XXX (Competent wizard should cope)", + "XX (Harmless / may be domesticated)" + ], ensure_ascii=False), + "hint_en": "It takes at least a hundred skilled wizards working together to subdue a single Nundu." + }, + { + "category": "creatures", + "difficulty": "easy", + "question_type": "text", + "question": "Welches kleine, pelzige Tierwesen wird magisch von allem Glitzernden und von Goldmünzen angezogen?", + "canonical_answer": "Niffler", + "accepted_answers": json.dumps(["Niffler", "Ein Niffler"], ensure_ascii=False), + "hint": "Newt Scamander hat eines dieser langnasigen Beuteltiere in seinem Koffer.", + "question_en": "Which small, furry magical creature is irresistibly drawn to anything shiny and gold coins?", + "answer_en": "Niffler", + "hint_en": "Newt Scamander keeps one of these long-snouted burrowing creatures in his case." + }, + { + "category": "creatures", + "difficulty": "medium", + "question_type": "multiple_choice", + "question": "Wie nennt man das unsichtbare geflügelte Pferd, das nur von Menschen gesehen werden kann, die den Tod bereits bewusst miterlebt haben?", + "canonical_answer": "Thestral", + "options": json.dumps(["Thestral", "Hippogreif", "Abraxaner", "Kelpie"], ensure_ascii=False), + "hint": "Sie ziehen die Kutschen vom Bahnhof Hogsmeade zum Schloss Hogwarts.", + "question_en": "What is the name of the invisible winged horse that can only be seen by those who have witnessed death?", + "answer_en": "Thestral", + "options_en": json.dumps(["Thestral", "Hippogriff", "Abraxan", "Kelpie"], ensure_ascii=False), + "hint_en": "They pull the carriages from Hogsmeade station up to Hogwarts Castle." + }, + + # --- BOOKS (Buchwissen) --- + { + "category": "books", + "difficulty": "hard", + "question_type": "text", + "question": "Wie lautet das vollständige Akronym der Organisation, die Hermine Granger im 4. Buch zur Befreiung der Hauselfen gründet?", + "canonical_answer": "B.ELFE.R.", + "accepted_answers": json.dumps(["B.ELFE.R.", "BELFER", "B.ELFE.R", "S.P.E.W.", "SPEW", "Bund für Elfenrechte"]), + "hint": "Es steht für 'Bund für Elfenrechte' (im Englischen: S.P.E.W.).", + "question_en": "What is the acronym of the organization Hermione Granger founds in her fourth year to campaign for house-elf rights?", + "answer_en": "S.P.E.W.", + "hint_en": "It stands for 'Society for the Promotion of Elfish Welfare' (in German: B.ELFE.R.)." + }, + { + "category": "books", + "difficulty": "hard", + "question_type": "multiple_choice", + "question": "Wer schenkt Harry im 4. Buch (im Gegensatz zum Film!) das Kraut 'Dianthuskraut' (Gillyweed) für die zweite Aufgabe des Trimagischen Turniers?", + "canonical_answer": "Dobby", + "options": json.dumps(["Dobby", "Neville Longbottom", "Alastor Moody", "Cedric Diggory"], ensure_ascii=False), + "hint": "Er stiehlt es aus Professor Snapes Vorratskammer, nachdem er ein inszeniertes Gespräch von Barty Crouch Jr. belauscht hat.", + "question_en": "In the Goblet of Fire book (unlike the movie!), who actually gives Harry the Gillyweed before the Second Task?", + "answer_en": "Dobby", + "options_en": json.dumps(["Dobby", "Neville Longbottom", "Alastor Moody", "Cedric Diggory"], ensure_ascii=False), + "hint_en": "He steals it from Professor Snape's private stores after overhearing a staged conversation by Barty Crouch Jr." + }, + { + "category": "books", + "difficulty": "hard", + "question_type": "text", + "question": "Wie heißt der Poltergeist von Hogwarts, der in allen sieben Büchern für Chaos sorgt, aber aus den Verfilmungen komplett gestrichen wurde?", + "canonical_answer": "Peeves", + "accepted_answers": json.dumps(["Peeves", "Poltergeist Peeves"]), + "hint": "Nur der Blutige Baron hat ihn wirklich unter Kontrolle.", + "question_en": "What is the name of the Hogwarts poltergeist who causes chaos throughout all seven books but was completely cut from the films?", + "answer_en": "Peeves", + "hint_en": "Only the Bloody Baron can truly keep him in line." + }, + { + "category": "books", + "difficulty": "hard", + "question_type": "multiple_choice", + "question": "Welches Rätsel stellt Professor Snape in Band 1 als Schutz vor dem Stein der Weisen auf, das im ersten Kinofilm komplett fehlt?", + "canonical_answer": "Ein Logikrätsel mit sieben Zaubertrank-Flaschen", + "options": json.dumps([ + "Ein Logikrätsel mit sieben Zaubertrank-Flaschen", + "Ein Duell gegen eine verzauberte Rüstung", + "Ein Schachbrett aus flüssigem Quecksilber", + "Eine Tür, die nur auf Parsel reagiert" + ], ensure_ascii=False), + "hint": "Hermine löst es mit reiner Logik, während ein schwarzes und ein violettes Feuer die Ausgänge blockieren.", + "question_en": "Which obstacle guarding the Philosopher's Stone was created by Professor Snape in the first book, but omitted from the film?", + "answer_en": "A logic puzzle with seven potion bottles", + "options_en": json.dumps([ + "A logic puzzle with seven potion bottles", + "A duel against an enchanted suit of armor", + "A chessboard made of liquid mercury", + "A door that only opens to Parseltongue" + ], ensure_ascii=False), + "hint_en": "Hermione solves it using pure logic while black and purple flames block the exits." + }, + { + "category": "books", + "difficulty": "hard", + "question_type": "text", + "question": "Wie heißt die Hauselfe der Familie Barty Crouch, die beim Quidditch-Weltpokal mit Harrys Zauberstab in der Hand gefunden wird?", + "canonical_answer": "Winky", + "accepted_answers": json.dumps(["Winky", "Hauselfe Winky"]), + "hint": "Ihr Name beginnt mit 'W' und sie verfällt nach ihrer Entlassung dem Butterbier.", + "question_en": "What is the name of the Crouch family's female house-elf who is found holding Harry's wand at the Quidditch World Cup?", + "answer_en": "Winky", + "hint_en": "Her name starts with 'W' and she later develops a Butterbeer habit in the Hogwarts kitchens." + }, + { + "category": "books", + "difficulty": "hard", + "question_type": "multiple_choice", + "question": "Wie heißt Tom Riddles Mutter mit vollem Geburtsnamen, deren tragische Geschichte Harry in Dumbledores Denkarium im 6. Buch erfährt?", + "canonical_answer": "Merope Gaunt", + "options": json.dumps(["Merope Gaunt", "Morfin Gaunt", "Kendra Dumbledore", "Eileen Prince"], ensure_ascii=False), + "hint": "Sie war eine Nachfahrin von Salazar Slytherin und verliebte sich in den Muggel Tom Riddle Senior.", + "question_en": "What was the maiden name of Tom Riddle's mother, whose tragic story Harry learns in Dumbledore's Pensieve in book 6?", + "answer_en": "Merope Gaunt", + "options_en": json.dumps(["Merope Gaunt", "Morfin Gaunt", "Kendra Dumbledore", "Eileen Prince"], ensure_ascii=False), + "hint_en": "She was a direct descendant of Salazar Slytherin and fell in love with the Muggle Tom Riddle Sr." + }, + { + "category": "books", + "difficulty": "medium", + "question_type": "multiple_choice", + "question": "Auf welcher Krankenstation im St.-Mungo-Hospital begegnen Harry, Ron und Hermine im 5. Buch zufällig Gilderoy Lockhart wieder?", + "canonical_answer": "Janus-Thickey-Station (Langzeit-Fluchschäden)", + "options": json.dumps([ + "Janus-Thickey-Station (Langzeit-Fluchschäden)", + "Dai-Llewellyn-Station (Tierbisse)", + "Artefakt-Unfälle im Erdgeschoss", + "Teekessel-Verbrennungsstation" + ], ensure_ascii=False), + "hint": "Auf derselben geschlossenen Station im 4. Stock liegen auch Frank und Alice Longbottom.", + "question_en": "On which ward of St Mungo's Hospital do Harry, Ron, and Hermione bump into Gilderoy Lockhart in the fifth book?", + "answer_en": "Janus Thickey Ward (Long-Term Spell Damage)", + "options_en": json.dumps([ + "Janus Thickey Ward (Long-Term Spell Damage)", + "Dai Llewellyn Ward (Creature Bites)", + "Artifact Accidents on the Ground Floor", + "Potion and Plant Poisoning Ward" + ], ensure_ascii=False), + "hint_en": "Frank and Alice Longbottom are patients on the very same fourth-floor ward." + }, + { + "category": "books", + "difficulty": "hard", + "question_type": "text", + "question": "Welchen Beruf übt Charlie Weasley in Rumänien aus?", + "canonical_answer": "Drachenwärter", + "accepted_answers": json.dumps([ + "Drachenwärter", "Drachenforscher", "Drachenzüchter", "Drachenpfleger", + "Er arbeitet mit Drachen", "Drachen", "Dragonologist", "Dragon keeper" + ], ensure_ascii=False), + "hint": "Er nimmt im 1. Buch den kleinen Norwegischen Stachelbuckel Norbert in seine Obhut.", + "question_en": "What is Charlie Weasley's profession in Romania?", + "answer_en": "Dragonkeeper", + "hint_en": "In the first book, his friends fly down to take Hagrid's Norwegian Ridgeback Norbert into their care." + }, + { + "category": "books", + "difficulty": "medium", + "question_type": "multiple_choice", + "question": "Was tut Harry im Buch 'Die Heiligtümer des Todes' mit dem Elderstab, bevor er ihn an seinen endgültigen Ort zurückbringt?", + "canonical_answer": "Er repariert seinen eigenen zerbrochenen Zauberstab", + "options": json.dumps([ + "Er repariert seinen eigenen zerbrochenen Zauberstab", + "Er zerbricht ihn und wirft ihn von der Brücke", + "Er schenkt ihn Ron Weasley", + "Er übergibt ihn dem Zaubereiministerium" + ], ensure_ascii=False), + "hint": "Mit 'Reparo' gelingt ihm etwas, was Ollivander zuvor für unmöglich gehalten hatte.", + "question_en": "In the Deathly Hallows book, what does Harry do with the Elder Wand before returning it to Dumbledore's tomb?", + "answer_en": "He repairs his own broken holly and phoenix wand", + "options_en": json.dumps([ + "He repairs his own broken holly and phoenix wand", + "He snaps it in half and throws it off the bridge", + "He gifts it to Ron Weasley", + "He locks it inside Gringotts" + ], ensure_ascii=False), + "hint_en": "Using 'Reparo', he accomplishes something Ollivander thought was impossible." + }, + { + "category": "books", + "difficulty": "hard", + "question_type": "multiple_choice", + "question": "Wie heißt der Kobold, der im 4. Buch von Ludo Bagman mit Leprechaun-Gold betrogen wird?", + "canonical_answer": "Gornuk (bzw. Ragnok / Griphooks Kollegen)", + "options": json.dumps(["Ragnok", "Bogrod", "Travers", "Yaxley"], ensure_ascii=False), + "hint": "Er trifft sich im Eberkopf mit Ludo Bagman, um seine Schulden einzufordern.", + "question_en": "Which goblin confronts Ludo Bagman in the Hog's Head inn after being paid in vanishing Leprechaun gold?", + "answer_en": "Ragnok", + "options_en": json.dumps(["Ragnok", "Bogrod", "Travers", "Yaxley"], ensure_ascii=False), + "hint_en": "He meets Ludo Bagman in the Hog's Head to demand repayment of his bet." + }, + { + "category": "books", + "difficulty": "medium", + "question_type": "text", + "question": "Welches magische Radio-Programm hören Ron, Harry und Hermine im 7. Buch auf der Flucht, für das man ein wechselndes Passwort braucht?", + "canonical_answer": "PotterWatch", + "accepted_answers": json.dumps(["PotterWatch", "Potter Watch", "Potterwatch"]), + "hint": "Moderiert wird die Sendung von Lee Jordan unter dem Decknamen 'Fluss' (River).", + "question_en": "What is the name of the secret rebel radio broadcast that Harry, Ron, and Hermione listen to while on the run in book 7?", + "answer_en": "Potterwatch", + "hint_en": "The program is hosted by Lee Jordan under the codename 'River'." + }, + { + "category": "books", + "difficulty": "hard", + "question_type": "multiple_choice", + "question": "Wer war die ursprüngliche Besitzerin des Diadems von Ravenclaw und zugleich deren Tochter (die Graue Dame)?", + "canonical_answer": "Helena Ravenclaw", + "options": json.dumps(["Helena Ravenclaw", "Rowena Ravenclaw", "Helga Hufflepuff", "Millicent Bagnold"], ensure_ascii=False), + "hint": "Sie stahl das Diadem ihrer Mutter und versteckte es in einem hohlen Baum in Albanien.", + "question_en": "What is the real name of the Grey Lady, the ghost of Ravenclaw Tower and daughter of Rowena Ravenclaw?", + "answer_en": "Helena Ravenclaw", + "options_en": json.dumps(["Helena Ravenclaw", "Rowena Ravenclaw", "Helga Hufflepuff", "Millicent Bagnold"], ensure_ascii=False), + "hint_en": "She stole her mother's diadem and hid it inside a hollow tree in Albania." + }, + + # --- MOVIES (Filmwissen) --- + { + "category": "movies", + "difficulty": "easy", + "question_type": "text", + "question": "Welcher britische Schauspieler verkörperte Harry Potter in allen acht Kinofilmen?", + "canonical_answer": "Daniel Radcliffe", + "accepted_answers": json.dumps(["Daniel Radcliffe", "Radcliffe"]), + "hint": "Er war bei den Dreharbeiten zum ersten Film erst elf Jahre alt.", + "question_en": "Which British actor portrayed Harry Potter across all eight feature films?", + "answer_en": "Daniel Radcliffe", + "hint_en": "He was only eleven years old when filming began on the first movie." + }, + { + "category": "movies", + "difficulty": "medium", + "question_type": "multiple_choice", + "question": "Welcher Regisseur inszenierte den dritten Kinofilm 'Harry Potter und der Gefangene von Askaban' (2004)?", + "canonical_answer": "Alfonso Cuarón", + "options": json.dumps(["Alfonso Cuarón", "Chris Columbus", "David Yates", "Mike Newell"], ensure_ascii=False), + "hint": "Der mexikanische Regisseur prägte den düsteren, künstlerischen Look des dritten Teils.", + "question_en": "Which director helmed the third movie, 'Harry Potter and the Prisoner of Azkaban' (2004)?", + "answer_en": "Alfonso Cuarón", + "options_en": json.dumps(["Alfonso Cuarón", "Chris Columbus", "David Yates", "Mike Newell"], ensure_ascii=False), + "hint_en": "The Mexican filmmaker gave the third installment its signature darker, atmospheric visual style." + }, + { + "category": "movies", + "difficulty": "medium", + "question_type": "text", + "question": "Welcher weltberühmte Filmkomponist schrieb das ikonische Hauptthema 'Hedwig's Theme' für die ersten Harry-Potter-Filme?", + "canonical_answer": "John Williams", + "accepted_answers": json.dumps(["John Williams", "Williams"]), + "hint": "Er komponierte auch die Musik für Star Wars, Indiana Jones und Jurassic Park.", + "question_en": "Which legendary film composer wrote the iconic main theme 'Hedwig's Theme' for the first Harry Potter movies?", + "answer_en": "John Williams", + "hint_en": "He also composed the scores for Star Wars, Indiana Jones, and Jurassic Park." + }, + { + "category": "movies", + "difficulty": "medium", + "question_type": "multiple_choice", + "question": "Welcher Schauspieler übernahm ab dem dritten Film die Rolle von Albus Dumbledore, nachdem Richard Harris verstorben war?", + "canonical_answer": "Michael Gambon", + "options": json.dumps(["Michael Gambon", "Ian McKellen", "Gary Oldman", "Ralph Fiennes"], ensure_ascii=False), + "hint": "Er spielte Dumbledore vom dritten bis zum achten Film.", + "question_en": "Which actor took over the role of Albus Dumbledore starting in the third film after Richard Harris passed away?", + "answer_en": "Michael Gambon", + "options_en": json.dumps(["Michael Gambon", "Ian McKellen", "Gary Oldman", "Ralph Fiennes"], ensure_ascii=False), + "hint_en": "He portrayed Dumbledore from Prisoner of Azkaban through Deathly Hallows – Part 2." + }, + { + "category": "movies", + "difficulty": "easy", + "question_type": "multiple_choice", + "question": "Über welches berühmte schottische Eisenbahnviadukt fährt der Hogwarts-Express in den Filmen?", + "canonical_answer": "Glenfinnan-Viadukt", + "options": json.dumps(["Glenfinnan-Viadukt", "Ribblehead-Viadukt", "Clifton-Brücke", "Forth Bridge"], ensure_ascii=False), + "hint": "Es liegt in den schottischen Highlands am Loch Shiel.", + "question_en": "Which famous Scottish railway viaduct does the Hogwarts Express cross in the movies?", + "answer_en": "Glenfinnan Viaduct", + "options_en": json.dumps(["Glenfinnan Viaduct", "Ribblehead Viaduct", "Clifton Bridge", "Forth Bridge"], ensure_ascii=False), + "hint_en": "It is located in the Scottish Highlands overlooking Loch Shiel." + }, + { + "category": "movies", + "difficulty": "hard", + "question_type": "multiple_choice", + "question": "Welche Augenfarbe hat Harry Potter in den Kinofilmen (weil Daniel Radcliffe die farbigen Kontaktlinsen nicht vertrug), obwohl sie in den Büchern leuchtend grün sind?", + "canonical_answer": "Blau", + "options": json.dumps(["Blau", "Braun", "Grau", "Bernsteinfarben"], ensure_ascii=False), + "hint": "Daniel Radcliffes natürliche Augenfarbe.", + "question_en": "What color are Harry Potter's eyes in the movies (because Daniel Radcliffe had an allergic reaction to contact lenses), even though they are bright green in the books?", + "answer_en": "Blue", + "options_en": json.dumps(["Blue", "Brown", "Hazel", "Amber"], ensure_ascii=False), + "hint_en": "It is Daniel Radcliffe's natural eye color." + }, + { + "category": "movies", + "difficulty": "hard", + "question_type": "text", + "question": "Welcher unvergessene britische Schauspieler verkörperte Severus Snape in allen acht Filmen?", + "canonical_answer": "Alan Rickman", + "accepted_answers": json.dumps(["Alan Rickman", "Rickman"]), + "hint": "J.K. Rowling verriet ihm schon vor dem Erscheinen des letzten Buches das Geheimnis hinter Snapes 'Always'.", + "question_en": "Which beloved British actor portrayed Severus Snape across all eight Harry Potter films?", + "answer_en": "Alan Rickman", + "hint_en": "J.K. Rowling privately told him the secret behind Snape's 'Always' years before the final book was published." + }, + { + "category": "movies", + "difficulty": "hard", + "question_type": "multiple_choice", + "question": "An welchem historischen englischen Bahnhof wurden die Außenaufnahmen von Hogsmeade Station in den ersten Filmen gedreht?", + "canonical_answer": "Goathland Station (North Yorkshire)", + "options": json.dumps([ + "Goathland Station (North Yorkshire)", + "Paddington Station (London)", + "Waverley Station (Edinburgh)", + "York Central Station" + ], ensure_ascii=False), + "hint": "Eine malerische Museumsbahnstation der North Yorkshire Moors Railway.", + "question_en": "At which historic English heritage railway station were the Hogsmeade Station scenes filmed in the early movies?", + "answer_en": "Goathland Station (North Yorkshire)", + "options_en": json.dumps([ + "Goathland Station (North Yorkshire)", + "Paddington Station (London)", + "Waverley Station (Edinburgh)", + "York Central Station" + ], ensure_ascii=False), + "hint_en": "A picturesque station on the North Yorkshire Moors Railway." + }, + { + "category": "movies", + "difficulty": "medium", + "question_type": "multiple_choice", + "question": "Auf welchem echten Londoner Bahnhof wurden die Szenen am Gleis 9¾ gedreht?", + "canonical_answer": "King's Cross", + "options": json.dumps(["King's Cross", "Waterloo", "Victoria Station", "Euston"], ensure_ascii=False), + "hint": "Die Außenfassade im 2. Film zeigt das benachbarte St Pancras Renaissance Hotel.", + "question_en": "At which real-life London railway station were the Platform 9¾ scenes filmed?", + "answer_en": "King's Cross", + "options_en": json.dumps(["King's Cross", "Waterloo", "Victoria Station", "Euston"], ensure_ascii=False), + "hint_en": "Today, fans can take photos with a luggage trolley disappearing into the brick wall there." + }, + { + "category": "movies", + "difficulty": "hard", + "question_type": "multiple_choice", + "question": "Wie heißt die fiktive Zauberrockband im 4. Film beim Weihnachtsball, deren Sänger von Pulp-Frontmann Jarvis Cocker gespielt wurde?", + "canonical_answer": "The Weird Sisters (Schwestern des Schicksals)", + "options": json.dumps([ + "The Weird Sisters (Schwestern des Schicksals)", + "Celestina Warbeck & the Banshees", + "The Hobgoblins", + "Frog Choir" + ], ensure_ascii=False), + "hint": "Auch Mitglieder der Band Radiohead spielten in dieser Filmszene in der Band mit!", + "question_en": "What is the name of the wizarding rock band at the Yule Ball in the fourth movie, fronted by Pulp's Jarvis Cocker?", + "answer_en": "The Weird Sisters", + "options_en": json.dumps([ + "The Weird Sisters", + "Celestina Warbeck & the Banshees", + "The Hobgoblins", + "The Frog Choir" + ], ensure_ascii=False), + "hint_en": "Members of Radiohead also appeared in the band lineup for that scene!" + }, + + # --- SPELLS & POTIONS (Zaubersprüche & Tränke) --- + { + "category": "spells", + "difficulty": "hard", + "question_type": "text", + "question": "Wie lautet der Gegenzauber zum Schwebezauber 'Levicorpus', mit dem man das Opfer wieder auf den Boden holt?", + "canonical_answer": "Liberacorpus", + "accepted_answers": json.dumps(["Liberacorpus", "Libera corpus"]), + "hint": "Harry findet sowohl den Zauber als auch den Gegenzauber handschriftlich im Buch des Halbblutprinzen.", + "question_en": "What is the counter-jinx to 'Levicorpus' that lowers the victim back to the ground?", + "answer_en": "Liberacorpus", + "hint_en": "Harry discovers both the spell and its counter-jinx written in the margins of the Half-Blood Prince's textbook." + }, + { + "category": "spells", + "difficulty": "hard", + "question_type": "multiple_choice", + "question": "Welcher seltene Stein aus dem Magen einer Ziege wirkt als universelles Gegengift gegen die meisten Zaubertrank-Gifte?", + "canonical_answer": "Bezoar", + "options": json.dumps(["Bezoar", "Mondstein", "Blutstein", "Flussstein"], ensure_ascii=False), + "hint": "Snape fragt Harry danach in seiner allerersten Zaubertränkestunde, und Harry rettet damit später Rons Leben.", + "question_en": "Which stone taken from the stomach of a goat acts as an antidote to most poisons?", + "answer_en": "Bezoar", + "options_en": json.dumps(["Bezoar", "Moonstone", "Bloodstone", "Runestone"], ensure_ascii=False), + "hint_en": "Snape quizzes Harry about it in his very first Potions lesson, and Harry later uses one to save Ron's life." + }, + { + "category": "spells", + "difficulty": "medium", + "question_type": "text", + "question": "Welcher dunkle Fluch, den der Halbblutprinz erfunden hat, schlitzt das Opfer wie mit einem unsichtbaren Schwert auf ('Für Feinde')?", + "canonical_answer": "Sectumsempra", + "accepted_answers": json.dumps(["Sectumsempra", "Sectum Sempra"]), + "hint": "Harry wendet ihn im 6. Teil im Waschraum gegen Draco Malfoy an.", + "question_en": "Which dark curse invented by the Half-Blood Prince slashes the target as if with an invisible sword ('For Enemies')?", + "answer_en": "Sectumsempra", + "hint_en": "Harry uses it against Draco Malfoy in the bathroom in year six." + }, + { + "category": "spells", + "difficulty": "medium", + "question_type": "multiple_choice", + "question": "Wie lange dauert die aufwendige Zubereitung des Vielsaft-Tranks (Polyjuice Potion) ungefähr?", + "canonical_answer": "Einen ganzen Monat", + "options": json.dumps(["Einen ganzen Monat", "Genau 24 Stunden", "Sieben Tage", "Nur eine Vollmondnacht"], ensure_ascii=False), + "hint": "Unter anderem muss Florfliegenkraut 21 Tage lang schmoren.", + "question_en": "Approximately how long does it take to brew Polyjuice Potion from start to finish?", + "answer_en": "One full month", + "options_en": json.dumps(["One full month", "Exactly 24 hours", "Seven days", "A single full-moon night"], ensure_ascii=False), + "hint_en": "Lacewing flies alone must stew for twenty-one days." + }, + + # --- HOGWARTS & PLACES --- + { + "category": "hogwarts", + "difficulty": "hard", + "question_type": "multiple_choice", + "question": "Wie gelangt man in die Schulküche von Hogwarts, die direkt unter der Großen Halle liegt?", + "canonical_answer": "Man kitzelt die Birne auf einem Obst-Gemälde", + "options": json.dumps([ + "Man kitzelt die Birne auf einem Obst-Gemälde", + "Man klopft dreimal auf ein Essigfass", + "Man sagt das Passwort 'Kürbispastete' zu einer Rüstung", + "Man zieht an einer goldenen Bratpfanne" + ], ensure_ascii=False), + "hint": "Die Frucht kichert daraufhin und verwandelt sich in einen grünen Türgriff.", + "question_en": "How do you open the secret entrance to the Hogwarts kitchens located beneath the Great Hall?", + "answer_en": "Tickle the pear in the painting of a bowl of fruit", + "options_en": json.dumps([ + "Tickle the pear in the painting of a bowl of fruit", + "Tap a vinegar barrel in the rhythm of Helga Hufflepuff", + "Whisper 'Pumpkin Pasties' to a suit of armor", + "Pull on a golden ladle" + ], ensure_ascii=False), + "hint_en": "The fruit giggles and transforms into a green door handle." + }, + { + "category": "hogwarts", + "difficulty": "hard", + "question_type": "multiple_choice", + "question": "Was unterscheidet den Eingang zum Ravenclaw-Gemeinschaftsraum von denen der anderen drei Häuser?", + "canonical_answer": "Man muss eine philosophische Rätselfrage des Türklopfers beantworten statt ein Passwort zu nennen", + "options": json.dumps([ + "Man muss eine philosophische Rätselfrage des Türklopfers beantworten statt ein Passwort zu nennen", + "Man kann nur auf einem Besen durch das Turmfenster hineinfliegen", + "Man muss seinen Zauberstab auf eine Waage legen", + "Die Tür öffnet sich nur bei Mondschein" + ], ensure_ascii=False), + "hint": "Der bronzene Adler-Türklopfer fragt z. B.: 'Was war zuerst da, der Phönix oder die Flamme?'", + "question_en": "How does entering the Ravenclaw common room differ from the other three Hogwarts houses?", + "answer_en": "You must answer a philosophical riddle posed by a bronze eagle knocker instead of giving a password", + "options_en": json.dumps([ + "You must answer a philosophical riddle posed by a bronze eagle knocker instead of giving a password", + "You can only enter by flying through the tower window on a broom", + "You must weigh your wand on an enchanted scale", + "The door only opens under moonlight" + ], ensure_ascii=False), + "hint_en": "For example, the bronze eagle asks: 'Which came first, the phoenix or the flame?'" + }, + { + "category": "hogwarts", + "difficulty": "medium", + "question_type": "text", + "question": "Unter welchem Baum auf dem Schulgelände von Hogwarts beginnt der geheime Tunnel, der direkt in die Heulende Hütte nach Hogsmeade führt?", + "canonical_answer": "Peitschende Weide", + "accepted_answers": json.dumps(["Peitschende Weide", "Die Peitschende Weide", "Whomping Willow"]), + "hint": "Er wurde gepflanzt, als Remus Lupin als Schüler nach Hogwarts kam.", + "question_en": "Beneath which violent tree on the Hogwarts grounds is the secret tunnel leading to the Shrieking Shack?", + "answer_en": "Whomping Willow", + "hint_en": "It was planted the year Remus Lupin arrived at Hogwarts as a student." + }, + { + "category": "hogwarts", + "difficulty": "easy", + "question_type": "text", + "question": "Wie heißt das einzige komplett von Zauberern bewohnte Dorf in Großbritannien, das in der Nähe von Hogwarts liegt?", + "canonical_answer": "Hogsmeade", + "accepted_answers": json.dumps(["Hogsmeade"]), + "hint": "Schüler ab der dritten Klasse dürfen es an bestimmten Wochenenden mit einer Unterschrift besuchen.", + "question_en": "What is the name of the only all-wizarding village in Britain, located near Hogwarts?", + "answer_en": "Hogsmeade", + "hint_en": "Students in their third year and above may visit it on certain weekends with a signed permission slip." + }, + + # --- QUIDDITCH --- + { + "category": "quidditch", + "difficulty": "hard", + "question_type": "multiple_choice", + "question": "Welches spektakuläre Flugmanöver führt Viktor Krum beim Finale der Quidditch-Weltmeisterschaft 1994 aus, um den gegnerischen Sucher Aidan Lynch in den Boden stürzen zu lassen?", + "canonical_answer": "Wronski-Bluff (Wronski Feint)", + "options": json.dumps([ + "Wronski-Bluff (Wronski Feint)", + "Porskoff-Ploy", + "Falkenkopf-Angriffsformation", + "Faultier-Rolle (Sloth Grip Roll)" + ], ensure_ascii=False), + "hint": "Der Sucher täuscht im Sturzflug vor, den Goldenen Schnatz am Boden gesehen zu haben, und zieht im letzten Moment hoch.", + "question_en": "Which famous diving maneuver does Viktor Krum execute during the 1994 Quidditch World Cup Final to trick Irish Seeker Aidan Lynch?", + "answer_en": "Wronski Feint", + "options_en": json.dumps([ + "Wronski Feint", + "Porskoff Ploy", + "Hawkshead Attacking Formation", + "Sloth Grip Roll" + ], ensure_ascii=False), + "hint_en": "The Seeker dives steeply toward the ground pretending to have spotted the Snitch, pulling up at the very last second." + }, + { + "category": "quidditch", + "difficulty": "hard", + "question_type": "multiple_choice", + "question": "Welches magische Wesen war vor der Erfindung des Goldenen Schnatzes das ursprüngliche Jagdziel beim Quidditch?", + "canonical_answer": "Der Goldene Schnatzer (Golden Snidget)", + "options": json.dumps([ + "Der Goldene Schnatzer (Golden Snidget)", + "Der Fwuuper", + "Der Augurey", + "Der Jobberknoll" + ], ensure_ascii=False), + "hint": "Ein kugelrunder kleiner goldener Vogel, der dadurch fast ausgerottet wurde, bevor Bowman Wright den metallenen Schnatz schmiedete.", + "question_en": "Before the invention of the metallic Golden Snitch, which small magical bird was hunted during Quidditch matches?", + "answer_en": "Golden Snidget", + "options_en": json.dumps([ + "Golden Snidget", + "Fwooper", + "Augurey", + "Jobberknoll" + ], ensure_ascii=False), + "hint_en": "The tiny spherical golden bird was nearly hunted to extinction before Bowman Wright forged the enchanted metal replacement." + }, + { + "category": "quidditch", + "difficulty": "medium", + "question_type": "multiple_choice", + "question": "Welche beiden Nationalmannschaften standen sich im Finale der Quidditch-Weltmeisterschaft 1994 gegenüber?", + "canonical_answer": "Irland und Bulgarien", + "options": json.dumps([ + "Irland und Bulgarien", + "England und Transsilvanien", + "Schottland und Frankreich", + "Irland und Rumänien" + ], ensure_ascii=False), + "hint": "Viktor Krum fing zwar den Schnatz, doch die andere Mannschaft gewann trotzdem mit 170 zu 160 Punkten!", + "question_en": "Which two national teams faced off in the final of the 1994 Quidditch World Cup?", + "answer_en": "Ireland and Bulgaria", + "options_en": json.dumps([ + "Ireland and Bulgaria", + "England and Transylvania", + "Scotland and France", + "Ireland and Romania" + ], ensure_ascii=False), + "hint_en": "Viktor Krum caught the Snitch, yet the opposing team still won the match 170 to 160!" + }, + { + "category": "quidditch", + "difficulty": "easy", + "question_type": "text", + "question": "Wie heißt das schnellste Rennbesen-Modell der Welt, das Harry im dritten Schuljahr anonym von seinem Paten Sirius Black geschenkt bekommt?", + "canonical_answer": "Feuerblitz", + "accepted_answers": json.dumps(["Feuerblitz", "Der Feuerblitz", "Firebolt"]), + "hint": "Er löst Harrys zerstörten Nimbus 2000 ab.", + "question_en": "What is the name of the world-class racing broom Harry receives anonymously from his godfather Sirius Black in his third year?", + "answer_en": "Firebolt", + "hint_en": "It replaces Harry's destroyed Nimbus 2000." + }, + + # --- CHARACTERS --- + { + "category": "characters", + "difficulty": "hard", + "question_type": "text", + "question": "In welches Tier verwandelt sich Rita Kimmkorn (Rita Skeeter) als nicht registrierter Animagus, um unbemerkt Gespräche zu belauschen?", + "canonical_answer": "Käfer", + "accepted_answers": json.dumps(["Käfer", "Ein Käfer", "Wasserkäfer", "Grüner Käfer", "Beetle"]), + "hint": "Hermine fängt sie am Ende des 4. Schuljahres in einem unzerbrechlichen Schraubglas.", + "question_en": "Into which insect does journalist Rita Skeeter transform as an unregistered Animagus to eavesdrop on conversations?", + "answer_en": "Beetle", + "hint_en": "Hermione traps her inside an unbreakable glass jar at the end of year four." + }, + { + "category": "characters", + "difficulty": "hard", + "question_type": "multiple_choice", + "question": "Wer hinterließ die geheimnisvolle Notiz mit den Initialen 'R.A.B.' in der falschen Horkrux-Höhle?", + "canonical_answer": "Regulus Arcturus Black", + "options": json.dumps([ + "Regulus Arcturus Black", + "Rodolphus Augustus Bell", + "Rufus Albus Bones", + "Rabastan Lestrange" + ], ensure_ascii=False), + "hint": "Der jüngere Bruder von Sirius Black, der mit Hilfe des Hauselfen Kreacher das echte Medaillon stahl.", + "question_en": "Who left the mysterious note signed 'R.A.B.' inside the fake Horcrux locket in the sea cave?", + "answer_en": "Regulus Arcturus Black", + "options_en": json.dumps([ + "Regulus Arcturus Black", + "Rodolphus Augustus Bell", + "Rufus Albus Bones", + "Rabastan Lestrange" + ], ensure_ascii=False), + "hint_en": "Sirius Black's younger brother, who stole the real locket with the help of the house-elf Kreacher." + } +] + + +def seed_lore_pack(): + db = SessionLocal() + try: + existing_questions = {q.question.strip().lower(): q for q in db.query(Question).all()} + max_sort = db.query(Question.sort_order).order_by(Question.sort_order.desc()).first() + next_sort = (max_sort[0] + 1) if max_sort and max_sort[0] else 100 + + added = 0 + for item in LORE_PACK_QUESTIONS: + q_key = item["question"].strip().lower() + if q_key in existing_questions: + continue + + new_q = Question( + category=item["category"], + difficulty=item["difficulty"], + question_type=item["question_type"], + question=item["question"], + canonical_answer=item["canonical_answer"], + accepted_answers=item.get("accepted_answers"), + options=item.get("options"), + hint=item.get("hint"), + question_en=item.get("question_en"), + answer_en=item.get("answer_en"), + options_en=item.get("options_en"), + hint_en=item.get("hint_en"), + status="active", + sort_order=next_sort, + submitted_by="System Lore Pack" + ) + db.add(new_q) + next_sort += 1 + added += 1 + + db.commit() + print(f"Lore Pack Seeding complete: {added} new questions added. Total active: {db.query(Question).filter(Question.status == 'active').count()}") + finally: + db.close() + + +if __name__ == "__main__": + seed_lore_pack() diff --git a/app/static/css/style.css b/app/static/css/style.css index 580ffd7..6959cee 100755 --- a/app/static/css/style.css +++ b/app/static/css/style.css @@ -1,1765 +1,1465 @@ -:root { - --bg-dark: #0b0b16; - --bg-card: rgba(20, 22, 40, 0.9); - --bg-card-hover: rgba(28, 31, 56, 0.95); - --gold: #c9a86a; - --gold-bright: #ffd700; - --gold-soft: #e6c98f; - --text-main: #f5f5f7; - --text-muted: #b8b8cc; - --parchment: #f6eedb; - --wax-red: #8b0000; - --success: #2ecc71; - --danger: #e74c3c; - --warning: #f39c12; - --border-gold: rgba(201, 168, 106, 0.35); - --header-height: 56px; -} - -* { - box-sizing: border-box; - margin: 0; - padding: 0; - -webkit-tap-highlight-color: transparent; -} - -body { - background-color: var(--bg-dark); - background-image: - radial-gradient(circle at 15% 15%, rgba(201, 168, 106, 0.08) 0%, transparent 45%), - radial-gradient(circle at 85% 85%, rgba(116, 0, 1, 0.08) 0%, transparent 50%); - color: var(--text-main); - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Georgia", "Times New Roman", serif; - min-height: 100vh; - display: flex; - flex-direction: column; - overflow-x: hidden; - line-height: 1.5; -} - -/* ========================================= - HEADER & NAVIGATION (Mobile-First) - ========================================= */ -.site-header { - background: rgba(11, 11, 22, 0.85); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - border-bottom: 1px solid var(--border-gold); - position: sticky; - top: 0; - z-index: 200; - width: 100%; -} - -.header-inner { - max-width: 900px; - margin: 0 auto; - padding: 0 16px; - height: var(--header-height); - display: flex; - justify-content: space-between; - align-items: center; - position: relative; -} - -.brand-link { - display: flex; - align-items: center; - gap: 8px; - text-decoration: none; - color: var(--gold); - font-weight: bold; - font-size: 1.15rem; - letter-spacing: 0.5px; -} - -.brand-sparkle { - font-size: 1.25rem; -} - -.header-right { - display: flex; - align-items: center; - 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 { - background: rgba(201, 168, 106, 0.15); - border: 1px solid var(--gold); - color: var(--gold-soft); - padding: 6px 12px; - border-radius: 20px; - cursor: pointer; - font-size: 0.85rem; - display: inline-flex; - align-items: center; - gap: 6px; - transition: all 0.2s ease; - min-height: 36px; -} - -.audio-btn:hover { - background: var(--gold); - color: #000; -} - -.menu-toggle-btn { - display: none; - background: transparent; - border: 1px solid var(--border-gold); - color: var(--gold); - font-size: 1.3rem; - width: 38px; - height: 38px; - border-radius: 8px; - cursor: pointer; - align-items: center; - justify-content: center; -} - -.nav-links { - display: flex; - gap: 18px; - align-items: center; -} - -.nav-link { - color: var(--text-muted); - text-decoration: none; - font-size: 0.9rem; - padding: 8px 12px; - border-radius: 6px; - transition: all 0.2s ease; - white-space: nowrap; -} - -.nav-link:hover { - color: var(--gold-bright); - background: rgba(201, 168, 106, 0.1); -} - -/* ========================================= - CONTAINER & LAYOUT - ========================================= */ -.container { - flex: 1; - max-width: 760px; - width: 100%; - margin: 20px auto; - padding: 0 16px; -} - -.admin-wrapper { - max-width: 900px; - width: 100%; - margin: 20px auto; - padding: 0 16px; -} - -.quiz-wrapper { - width: 100%; -} - -.quiz-header { - text-align: center; - margin-bottom: 20px; -} - -.glow-title { - color: var(--gold); - font-size: clamp(1.35rem, 5vw, 2.1rem); - letter-spacing: 0.5px; - text-shadow: 0 0 14px rgba(201, 168, 106, 0.4); - margin-bottom: 6px; - word-break: break-word; -} - -.subtitle { - color: var(--text-muted); - font-size: clamp(0.85rem, 3vw, 1rem); - font-style: italic; - margin-bottom: 15px; -} - -/* ========================================= - CARD COMPONENT - ========================================= */ -.card { - background: var(--bg-card); - border: 1px solid var(--border-gold); - border-radius: 14px; - padding: clamp(16px, 4vw, 28px); - box-shadow: 0 8px 25px rgba(0, 0, 0, 0.45); - backdrop-filter: blur(8px); - -webkit-backdrop-filter: blur(8px); - margin-bottom: 20px; - width: 100%; -} - -/* ========================================= - ANTIQUE HOGWARTS CLOSED ENVELOPE & LETTER REVEAL - ========================================= */ -.intro-parchment-wrapper { - background: transparent !important; - border: none !important; - box-shadow: none !important; - padding: 0 !important; - max-width: 600px; - margin: 0 auto; -} - -.antique-envelope-scene { - position: relative; - width: min(440px, 92vw); - height: min(260px, 58vw); - margin: 25px auto 35px; - perspective: 1200px; - perspective-origin: center top; -} - -.envelope-base { - position: relative; - width: 100%; - height: 100%; - border-radius: 6px; - box-shadow: - 0 20px 45px rgba(0, 0, 0, 0.75), - 0 4px 14px rgba(0, 0, 0, 0.5); - transition: transform 0.6s ease; -} - -/* Interior of envelope (visible when flap opens and letter rises) */ -.envelope-interior { - position: absolute; - inset: 0; - border-radius: 6px; - background-color: #2b1d12; - background-image: - radial-gradient(circle at 50% 50%, rgba(0, 0, 0, 0.25) 0%, rgba(0, 0, 0, 0.65) 100%); - border: 2px solid #7a5c36; - z-index: 1; -} - -/* The Letter inside the envelope (slides up when opened) */ -.letter-sheet { - position: absolute; - bottom: 8px; - left: 4%; - width: 92%; - height: 92%; - background-color: #fcf8ee; - background-image: - radial-gradient(ellipse at 50% 25%, rgba(255, 255, 255, 0.6) 0%, rgba(210, 180, 130, 0.25) 100%); - background-position: center; - border-radius: 4px; - border: 1.5px solid #8e6c43; - padding: 16px 18px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.35); - z-index: 2; - transition: transform 1.1s cubic-bezier(0.2, 0.8, 0.25, 1) 0.35s, box-shadow 0.8s ease, z-index 0.2s 0.35s; - overflow: hidden; - text-align: center; - outline: 1px dashed rgba(130, 95, 55, 0.45); - outline-offset: -5px; -} - -/* Front Pocket of envelope (covers the lower portion of the letter) */ -.envelope-front-pocket { - position: absolute; - inset: 0; - border-radius: 6px; - background-color: #ebdcb9; - background-image: - radial-gradient(ellipse at 50% 65%, rgba(255, 255, 255, 0.35) 0%, rgba(160, 120, 60, 0.2) 100%); - clip-path: polygon(0 32%, 50% 54%, 100% 32%, 100% 100%, 0 100%); - z-index: 4; - border: 2px solid #8e6c43; - filter: drop-shadow(0 -3px 6px rgba(60, 40, 15, 0.3)); - display: flex; - align-items: flex-end; - justify-content: center; - padding-bottom: 20px; - pointer-events: none; -} - -.envelope-sketch-border { - position: absolute; - bottom: 8px; - left: 8px; - right: 8px; - height: 48%; - border: 1px dashed rgba(120, 85, 45, 0.4); - border-radius: 3px; - pointer-events: none; -} - -/* Handwritten Address in Emerald Ink on Envelope Front */ -.envelope-address-block { - position: relative; - z-index: 5; - text-align: center; - font-family: 'Marck Script', 'Caveat', cursive; - color: #0d3820; /* Authentic Hogwarts Emerald Ink */ - padding: 0 15px; - text-shadow: 0 0 1px rgba(13, 56, 32, 0.35); -} - -.addr-school { - font-size: clamp(0.92rem, 3.2vw, 1.15rem); - font-weight: 700; - color: #082816; - margin-bottom: 2px; -} - -.addr-recipient { - font-size: clamp(1.15rem, 3.8vw, 1.4rem); - font-weight: 700; - color: #0b3a20; - margin-bottom: 2px; -} - -.addr-location { - font-size: clamp(0.85rem, 2.8vw, 1.02rem); - font-style: italic; - color: #1e4530; -} - -/* Top Triangular Flap (folds down over the pocket) */ -.envelope-closing-flap { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: #dfcd9f; - background-image: - radial-gradient(ellipse at 50% 25%, rgba(255, 255, 255, 0.4) 0%, rgba(150, 110, 55, 0.25) 100%); - clip-path: polygon(0 0, 100% 0, 50% 57%); - transform-origin: top center; - transform-style: preserve-3d; - transition: transform 0.75s cubic-bezier(0.4, 0, 0.2, 1); - z-index: 6; - border-top: 2px solid #8e6c43; - filter: drop-shadow(0 5px 8px rgba(50, 35, 10, 0.5)); -} - -.flap-sketch-line { - position: absolute; - inset: 4px; - clip-path: polygon(2% 2%, 98% 2%, 50% 54%); - border-top: 1px dashed rgba(120, 85, 45, 0.4); - pointer-events: none; -} - -/* Sketched Wax Seal placed right over the flap tip */ -.sketched-wax-seal { - position: absolute; - top: 56%; - left: 50%; - transform: translate(-50%, -50%) rotate(-3deg); - width: 72px; - height: 72px; - border-radius: 50%; - cursor: pointer; - z-index: 7; - transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.3s ease, opacity 0.4s ease; - box-shadow: - 0 8px 20px rgba(0, 0, 0, 0.7), - 0 2px 5px rgba(0, 0, 0, 0.45); -} - -.sketched-wax-seal:hover { - transform: translate(-50%, -50%) rotate(0deg) scale(1.1); - box-shadow: - 0 12px 28px rgba(0, 0, 0, 0.8), - 0 0 16px rgba(218, 165, 32, 0.45); -} - -.seal-crest-img { - width: 100%; - height: 100%; - border-radius: 50%; - object-fit: cover; - display: block; - border: 3px solid #4a0d0d; - box-shadow: inset 0 0 12px rgba(0, 0, 0, 0.7); -} - -.seal-emboss-ring { - position: absolute; - inset: 2px; - border-radius: 50%; - border: 1.5px solid rgba(255, 215, 0, 0.25); - box-shadow: inset 0 2px 4px rgba(255, 255, 255, 0.2), inset 0 -3px 6px rgba(0, 0, 0, 0.6); - pointer-events: none; -} - -.seal-glow { - position: absolute; - inset: -6px; - border-radius: 50%; - background: radial-gradient(circle, rgba(255, 215, 0, 0.35) 0%, transparent 70%); - opacity: 0; - transition: opacity 0.3s ease; - pointer-events: none; -} - -.sketched-wax-seal:hover .seal-glow { - opacity: 1; -} - -/* Open state: Flap opens, seal dissolves, letter slides out */ -.envelope-opened .sketched-wax-seal { - animation: breakSealAnim 0.7s forwards; -} - -.envelope-opened .envelope-closing-flap { - transform: rotateX(180deg); - z-index: 1; - filter: drop-shadow(0 -4px 6px rgba(50, 35, 10, 0.4)); -} - -.envelope-opened .letter-sheet { - transform: translateY(-135px) scale(1.06); - z-index: 10; - box-shadow: - 0 20px 48px rgba(0, 0, 0, 0.85), - 0 0 25px rgba(201, 168, 106, 0.35); -} - -@keyframes breakSealAnim { - 0% { transform: translate(-50%, -50%) rotate(-3deg) scale(1); opacity: 1; filter: none; } - 50% { transform: translate(-50%, -50%) rotate(10deg) scale(1.2); opacity: 0.8; filter: drop-shadow(0 0 25px #ffd700); } - 100% { transform: translate(-50%, -50%) rotate(25deg) scale(0.3); opacity: 0; filter: drop-shadow(0 0 40px #ffd700); } -} - -/* Letter Content Styles */ -.letter-header { - margin-bottom: 8px; -} - -.letter-crest-box { - width: 48px; - height: 48px; - margin: 0 auto 4px; - border-radius: 50%; - overflow: hidden; - box-shadow: 0 2px 6px rgba(90, 60, 20, 0.25); - border: 1px solid rgba(139, 105, 60, 0.3); -} - -.letter-crest-sketch { - width: 100%; - height: 100%; - object-fit: cover; - filter: sepia(0.65) contrast(1.15) brightness(0.92); -} - -.letter-school-name { - font-family: 'MedievalSharp', Georgia, serif; - font-size: clamp(0.95rem, 2.8vw, 1.25rem); - color: #2c1a0c; - text-transform: uppercase; - letter-spacing: 1.2px; - font-weight: 700; - margin-bottom: 2px; - text-shadow: 0 1px 1px rgba(255, 255, 255, 0.6); -} - -.letter-headmaster { - font-family: 'Caveat', 'Georgia', cursive; - font-size: 0.95rem; - color: #4a331c; - font-style: italic; - line-height: 1.25; - margin-bottom: 6px; -} - -.letter-headmaster span { - font-size: 0.82rem; - color: #6a4c28; -} - -.letter-ornament-line { - display: flex; - align-items: center; - justify-content: center; - gap: 6px; - color: #8c6838; - margin: 4px auto 8px; - max-width: 260px; -} - -.ornament-rule { - flex: 1; - height: 1px; - background: linear-gradient(to right, transparent, #8c6838, transparent); -} - -.ornament-leaf, .ornament-shield { - font-size: 0.85rem; -} - -/* Handwritten Body in Emerald Ink */ -.letter-body-handwritten { - font-family: 'Marck Script', 'Caveat', cursive; - color: #0f3d24; /* Authentic Hogwarts Emerald Ink */ - text-align: left; - padding: 0 4px; - line-height: 1.5; - text-shadow: 0 0 1px rgba(15, 61, 36, 0.25); -} - -.letter-salutation { - font-size: clamp(1.15rem, 3.5vw, 1.4rem); - font-weight: 700; - margin-bottom: 6px; - color: #0a301c; -} - -.letter-paragraph { - font-size: clamp(1.02rem, 3.0vw, 1.2rem); - margin-bottom: 8px; - text-indent: 1.2em; -} - -.letter-paragraph strong { - color: #052414; - font-weight: 700; -} - -/* Signature Block */ -.letter-signature-block { - margin-top: 10px; - text-align: right; - padding-right: 8px; - font-family: 'Marck Script', 'Caveat', cursive; - color: #0f3d24; -} - -.letter-closing { - font-size: 1.1rem; - font-style: italic; - color: #2b4533; - margin-bottom: 2px; -} - -.letter-signature-ink { - font-size: clamp(1.4rem, 4.2vw, 1.8rem); - font-weight: 700; - color: #082816; - letter-spacing: 0.5px; - transform: rotate(-2deg); - display: inline-block; - margin-bottom: 1px; - text-shadow: 0 0 1px rgba(8, 40, 22, 0.35); -} - -.letter-sign-title { - font-size: 0.88rem; - color: #4a6352; - font-style: italic; -} - -/* Controls Below Parchment */ -.intro-controls-panel { - margin-top: 20px; - text-align: center; -} - -.intro-subnote { - font-size: 0.95rem; - color: var(--text-muted); - line-height: 1.5; - margin-bottom: 14px; -} - -.btn-break-seal { - font-size: 1.12rem; - padding: 14px 28px; - box-shadow: 0 6px 20px rgba(201, 168, 106, 0.4); - transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); -} - -.btn-break-seal:hover { - transform: translateY(-2px) scale(1.02); - box-shadow: 0 10px 28px rgba(201, 168, 106, 0.6); -} - -@media (max-width: 480px) { - .antique-envelope-scene { - width: min(320px, 92vw); - height: min(205px, 58vw); - margin: 15px auto 25px; - } - .sketched-wax-seal { - width: 60px; - height: 60px; - top: 56%; - } - .envelope-opened .letter-sheet { - transform: translateY(-100px) scale(1.04); - } - .envelope-front-pocket { - padding-bottom: 14px; - } -} - -.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 - ========================================= */ -.btn { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 12px 22px; - border-radius: 8px; - font-size: 0.95rem; - font-weight: bold; - cursor: pointer; - text-decoration: none; - transition: all 0.2s ease; - border: none; - font-family: inherit; - min-height: 46px; - touch-action: manipulation; -} - -.btn-primary { - background: linear-gradient(135deg, var(--gold) 0%, #b8860b 100%); - color: #0c0d14; - box-shadow: 0 4px 15px rgba(201, 168, 106, 0.3); -} - -.btn-primary:hover:not(:disabled) { - background: linear-gradient(135deg, #ffd700 0%, var(--gold) 100%); - box-shadow: 0 6px 20px rgba(201, 168, 106, 0.5); - transform: translateY(-2px); -} - -.btn-secondary { - background: rgba(255, 255, 255, 0.08); - color: var(--gold-soft); - border: 1px solid var(--gold); -} - -.btn-secondary:hover:not(:disabled) { - background: rgba(201, 168, 106, 0.2); -} - -.btn-danger { - background: rgba(231, 76, 60, 0.2); - color: #ff7675; - border: 1px solid var(--danger); -} - -.btn-danger:hover:not(:disabled) { - background: var(--danger); - color: #fff; -} - -.btn-success { - background: rgba(46, 204, 113, 0.2); - color: #55efc4; - border: 1px solid var(--success); -} - -.btn-success:hover:not(:disabled) { - background: var(--success); - color: #000; -} - -.btn-large { - font-size: 1.05rem; - padding: 14px 28px; - min-height: 50px; -} - -.btn-sm { - font-size: 0.85rem; - padding: 8px 14px; - min-height: 38px; -} - -.btn-group { - display: flex; - gap: 10px; - justify-content: center; - margin-top: 18px; - width: 100%; -} - -.btn:disabled { - opacity: 0.6; - cursor: not-allowed; - transform: none !important; -} - -/* ========================================= - STATUS & PROGRESS - ========================================= */ -.status-panel { - margin-bottom: 16px; - width: 100%; -} - -.progress-info { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 8px; - font-size: clamp(0.8rem, 3vw, 0.95rem); -} - -.points-badge { - color: var(--gold-bright); - font-weight: bold; - text-shadow: 0 0 8px rgba(255, 215, 0, 0.4); -} - -.progress-container { - width: 100%; - height: 10px; - background: rgba(255, 255, 255, 0.08); - border: 1px solid var(--border-gold); - border-radius: 8px; - overflow: hidden; -} - -.progress-bar { - height: 100%; - width: 0%; - background: linear-gradient(90deg, var(--gold) 0%, #ffd700 100%); - box-shadow: 0 0 8px rgba(255, 215, 0, 0.5); - transition: width 0.35s ease; -} - -/* ========================================= - QUESTION CARD & ANSWERS - ========================================= */ -.question-card { - text-align: center; -} - -.question-meta { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 14px; - gap: 8px; -} - -.meta-left { - display: flex; - align-items: center; - gap: 8px; - flex-wrap: wrap; -} - -.badge { - padding: 4px 10px; - border-radius: 12px; - font-size: 0.75rem; - font-weight: bold; - background: rgba(201, 168, 106, 0.15); - border: 1px solid var(--gold); - color: var(--gold-soft); - 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 { - background: rgba(243, 156, 18, 0.2); - border-color: var(--warning); - color: #f39c12; -} - -.badge-success { - background: rgba(46, 204, 113, 0.2); - border-color: var(--success); - color: #2ecc71; -} - -.badge-danger { - background: rgba(231, 76, 60, 0.2); - border-color: var(--danger); - color: #e74c3c; -} - -.badge-muted { - background: rgba(255, 255, 255, 0.1); - border-color: rgba(255, 255, 255, 0.2); - color: var(--text-muted); -} - -.attempt-badge { - font-size: 0.8rem; - color: var(--text-muted); - white-space: nowrap; -} - -.timer-badge { - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; - background: rgba(243, 156, 18, 0.15); - border: 1px solid #f39c12; - color: #ffd700; - font-weight: 700; - letter-spacing: 0.5px; -} - -.question-text { - font-size: clamp(1.15rem, 4.5vw, 1.45rem); - line-height: 1.4; - text-align: center; - margin: 15px 0 20px; - color: var(--text-main); - word-break: break-word; -} - -/* Freitext Input */ -.answer-form { - display: flex; - flex-direction: column; - align-items: center; - width: 100%; -} - -.input-answer { - width: 100%; - max-width: 450px; - padding: 14px; - font-size: 16px; /* Prevents iOS auto-zoom */ - text-align: center; - border-radius: 8px; - border: 2px solid var(--border-gold); - background: rgba(0, 0, 0, 0.35); - color: var(--text-main); - outline: none; - transition: all 0.2s ease; - font-family: inherit; - min-height: 48px; -} - -.input-answer:focus { - border-color: var(--gold); - box-shadow: 0 0 12px rgba(201, 168, 106, 0.4); -} - -/* Multiple Choice Grid */ -.choice-group { - width: 100%; - margin-top: 5px; -} - -.choice-grid { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 12px; - width: 100%; -} - -.choice-btn { - background: rgba(255, 255, 255, 0.05); - border: 2px solid var(--border-gold); - border-radius: 10px; - padding: 12px 14px; - color: var(--text-main); - font-size: clamp(0.9rem, 3.5vw, 1.05rem); - font-family: inherit; - cursor: pointer; - text-align: left; - display: flex; - align-items: center; - gap: 10px; - transition: all 0.2s ease; - min-height: 52px; - width: 100%; - word-break: break-word; - touch-action: manipulation; -} - -.choice-btn:hover:not(:disabled) { - background: rgba(201, 168, 106, 0.2); - border-color: var(--gold); - transform: translateY(-2px); -} - -.choice-btn:active:not(:disabled) { - transform: scale(0.98); -} - -.choice-letter { - display: inline-flex; - align-items: center; - justify-content: center; - width: 28px; - height: 28px; - background: rgba(201, 168, 106, 0.2); - border: 1px solid var(--gold); - border-radius: 50%; - color: var(--gold-bright); - font-weight: bold; - font-size: 0.85rem; - flex-shrink: 0; -} - -.choice-text { - flex: 1; -} - -.choice-selected { - border-color: var(--gold-bright); - background: rgba(201, 168, 106, 0.3); -} - -.choice-correct { - background: rgba(46, 204, 113, 0.25) !important; - border-color: #2ecc71 !important; - color: #55efc4 !important; - box-shadow: 0 0 12px rgba(46, 204, 113, 0.6) !important; -} - -.choice-wrong { - background: rgba(231, 76, 60, 0.25) !important; - border-color: #e74c3c !important; - color: #ff7675 !important; - box-shadow: 0 0 12px rgba(231, 76, 60, 0.5) !important; -} - -.hint-box { - margin-top: 15px; - padding: 12px 14px; - border-radius: 8px; - background: rgba(201, 168, 106, 0.1); - border-left: 4px solid var(--gold); - font-size: 0.9rem; - color: var(--gold-soft); - text-align: left; - word-break: break-word; -} - -.feedback-box { - margin-top: 15px; - font-size: clamp(0.95rem, 3.5vw, 1.1rem); - text-align: center; - min-height: 28px; - font-weight: bold; - word-break: break-word; -} - -.feedback-correct { - color: var(--success); - text-shadow: 0 0 8px rgba(46, 204, 113, 0.4); -} - -.feedback-wrong { - color: var(--danger); -} - -.feedback-revealed { - color: var(--warning); - font-size: 0.9rem; - margin-top: 4px; -} - -/* ========================================= - ANIMATIONS - ========================================= */ -.sparkle-effect { - animation: sparkle-glow 0.8s ease forwards; -} - -@keyframes sparkle-glow { - 0% { box-shadow: 0 0 5px rgba(255, 215, 0, 0.2); } - 50% { box-shadow: 0 0 25px rgba(255, 215, 0, 0.8), inset 0 0 15px rgba(255, 215, 0, 0.4); border-color: #ffd700; } - 100% { box-shadow: 0 8px 25px rgba(0, 0, 0, 0.45); } -} - -.shake-effect { - animation: shake 0.4s ease; -} - -@keyframes shake { - 0%, 100% { transform: translateX(0); } - 20%, 60% { transform: translateX(-6px); } - 40%, 80% { transform: translateX(6px); } -} - -/* ========================================= - FINALE PAGE - ========================================= */ -.finale-card { - text-align: center; -} - -.finale-header { - display: flex; - justify-content: center; - align-items: center; - gap: 10px; - margin-bottom: 15px; -} - -.sparkle-icon { - font-size: 1.5rem; -} - -.score-banner { - background: rgba(201, 168, 106, 0.1); - border: 1px dashed var(--gold); - border-radius: 8px; - padding: 14px; - margin-bottom: 20px; -} - -.score-highlight { - font-size: clamp(1rem, 4vw, 1.25rem); -} - -.gold-text { - color: var(--gold-bright); - font-weight: bold; - font-size: 1.35rem; -} - -.reward-box { - background: rgba(0, 0, 0, 0.3); - border-radius: 10px; - padding: 18px 14px; - margin-bottom: 20px; - border-left: 4px solid var(--gold); - text-align: left; -} - -.reward-box h2 { - color: var(--gold-soft); - font-size: clamp(1.1rem, 4vw, 1.35rem); - margin-bottom: 12px; -} - -.reward-description { - font-size: clamp(0.95rem, 3.5vw, 1.05rem); - line-height: 1.6; - margin-bottom: 12px; - color: var(--text-main); -} - -.love-note { - font-style: italic; - color: var(--gold); - font-size: 1rem; -} - -.time-highlight { - margin-top: 8px; - font-size: clamp(0.9rem, 3.2vw, 1.05rem); - color: var(--text-muted); -} - -/* Tiered evaluation styling */ -.tier-box { - position: relative; - border-radius: 14px; - padding: 24px 18px; - margin: 20px 0; - border: 2px solid var(--border-gold); - background: radial-gradient(circle at top, rgba(201, 168, 106, 0.12) 0%, rgba(15, 17, 30, 0.85) 100%); - text-align: center; -} - -.tier-badge-container { - display: flex; - justify-content: center; - margin-bottom: 14px; -} - -.tier-grade-badge { - display: inline-flex; - align-items: center; - justify-content: center; - width: 64px; - height: 64px; - border-radius: 50%; - font-size: 2rem; - font-weight: 900; - text-shadow: 0 2px 4px rgba(0,0,0,0.6); - box-shadow: 0 4px 15px rgba(0,0,0,0.5); - border: 3px solid; -} - -.tier-outstanding { - background: radial-gradient(circle, #ffd700, #b8860b); - color: #0b0b16; - border-color: #fff275; - box-shadow: 0 0 20px rgba(255, 215, 0, 0.6); -} - -.tier-exceeds { - background: radial-gradient(circle, #2ecc71, #27ae60); - color: #0b0b16; - border-color: #a8f5cc; - box-shadow: 0 0 20px rgba(46, 204, 113, 0.5); -} - -.tier-acceptable { - background: radial-gradient(circle, #3498db, #2980b9); - color: #ffffff; - border-color: #a0d3f8; - box-shadow: 0 0 20px rgba(52, 152, 219, 0.5); -} - -.tier-poor { - background: radial-gradient(circle, #e67e22, #d35400); - color: #ffffff; - border-color: #f7b278; - box-shadow: 0 0 20px rgba(230, 126, 34, 0.5); -} - -.tier-troll { - background: radial-gradient(circle, #7f8c8d, #34495e); - color: #ff7675; - border-color: #e74c3c; - box-shadow: 0 0 20px rgba(231, 76, 60, 0.5); -} - -.tier-subtitle { - font-style: italic; - color: var(--gold-soft); - margin-bottom: 12px; - font-size: 1rem; -} - -/* Leaderboard entry card & form on finale */ -.leaderboard-entry-card { - background: rgba(14, 16, 32, 0.85); - border: 1px dashed var(--gold); - padding: 20px; - margin-top: 20px; - text-align: center; -} - -.input-with-btn { - display: flex; - gap: 10px; - max-width: 480px; - margin: 0 auto; - width: 100%; -} - -.input-with-btn .input-text { - flex: 1; - text-align: left; -} - -.btn-dice { - background: rgba(201, 168, 106, 0.18); - border: 1px solid var(--gold); - color: var(--gold-bright); - padding: 0 14px; - border-radius: 8px; - cursor: pointer; - font-size: 0.95rem; - font-family: inherit; - font-weight: bold; - display: inline-flex; - align-items: center; - gap: 6px; - justify-content: center; - transition: all 0.2s ease; - min-height: 46px; - white-space: nowrap; -} - -.btn-dice:hover { - background: var(--gold); - color: #0b0b16; - box-shadow: 0 0 12px rgba(201, 168, 106, 0.5); - transform: translateY(-1px); -} - -/* Leaderboard tables & filters */ -.diff-filter-tabs { - display: flex; - flex-wrap: wrap; - gap: 8px; - justify-content: center; - margin: 15px 0; -} - -.diff-filter-tabs .tab-btn { - border-radius: 20px; - padding: 8px 16px; - font-size: 0.88rem; - background: rgba(255, 255, 255, 0.05); - border: 1px solid var(--border-gold); - color: var(--text-muted); -} - -.diff-filter-tabs .tab-btn.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); -} - -.table-responsive { - width: 100%; - overflow-x: auto; - -webkit-overflow-scrolling: touch; -} - -.lb-table { - width: 100%; - border-collapse: collapse; - text-align: left; - font-size: 0.9rem; -} - -.lb-table th { - background: rgba(201, 168, 106, 0.12); - color: var(--gold-soft); - padding: 10px 12px; - border-bottom: 2px solid var(--border-gold); - white-space: nowrap; -} - -.lb-table td { - padding: 10px 12px; - border-bottom: 1px solid rgba(201, 168, 106, 0.15); - color: var(--text-main); -} - -.lb-table tr:hover td { - background: rgba(201, 168, 106, 0.05); -} - -.lb-rank { - font-weight: bold; - color: var(--gold); -} - -.lb-rank-1 { color: #ffd700; font-size: 1.1rem; } -.lb-rank-2 { color: #c0c0c0; font-size: 1.05rem; } -.lb-rank-3 { color: #cd7f32; font-size: 1.05rem; } - -/* ========================================= - FORMS & INPUTS - ========================================= */ -.magic-form { - display: flex; - flex-direction: column; - gap: 16px; - text-align: left; - width: 100%; -} - -.form-group { - display: flex; - flex-direction: column; - gap: 6px; - width: 100%; -} - -.form-group label { - font-size: 0.9rem; - font-weight: bold; - color: var(--gold-soft); -} - -.input-text, .textarea, select.input-text { - padding: 12px 14px; - border-radius: 8px; - border: 1px solid var(--border-gold); - background: rgba(0, 0, 0, 0.4); - color: var(--text-main); - font-size: 16px; /* Prevents auto zoom */ - font-family: inherit; - outline: none; - width: 100%; - min-height: 46px; -} - -.input-text:focus, .textarea:focus { - border-color: var(--gold); - box-shadow: 0 0 8px rgba(201, 168, 106, 0.3); -} - -.input-text-sm { - padding: 8px 10px; - border-radius: 6px; - border: 1px solid var(--border-gold); - background: rgba(0,0,0,0.3); - color: var(--text-main); - font-size: 15px; - min-height: 38px; - width: 100%; -} - -.field-help { - font-size: 0.8rem; - color: var(--text-muted); -} - -.info-alert { - background: rgba(41, 128, 185, 0.15); - border-left: 4px solid #3498db; - padding: 14px; - border-radius: 8px; - margin-bottom: 20px; - font-size: 0.9rem; - line-height: 1.5; -} - -.alert-danger { - background: rgba(231, 76, 60, 0.2); - border: 1px solid var(--danger); - color: #ff7675; - padding: 12px; - border-radius: 6px; - margin-bottom: 16px; - text-align: center; - font-size: 0.9rem; -} - -/* ========================================= - ADMIN COCKPIT (Mobile Responsive) - ========================================= */ -.admin-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 20px; - flex-wrap: wrap; - gap: 12px; -} - -.admin-top-actions { - display: flex; - gap: 8px; -} - -/* Horizontal Scrollable Tabs on Mobile */ -.tabs-nav { - display: flex; - gap: 6px; - border-bottom: 1px solid var(--border-gold); - margin-bottom: 20px; - overflow-x: auto; - -webkit-overflow-scrolling: touch; - scrollbar-width: none; /* Firefox */ - padding-bottom: 2px; -} - -.tabs-nav::-webkit-scrollbar { - display: none; /* Chrome/Safari */ -} - -.tab-btn { - background: rgba(255, 255, 255, 0.05); - border: none; - color: var(--text-muted); - padding: 10px 14px; - border-top-left-radius: 8px; - border-top-right-radius: 8px; - cursor: pointer; - font-size: 0.85rem; - font-family: inherit; - font-weight: bold; - display: inline-flex; - align-items: center; - gap: 6px; - white-space: nowrap; - flex-shrink: 0; - min-height: 42px; - touch-action: manipulation; -} - -.tab-btn.active { - background: var(--bg-card); - color: var(--gold); - border: 1px solid var(--border-gold); - border-bottom: 1px solid transparent; -} - -.tab-content { - display: none; -} - -.tab-content.active { - display: block; -} - -.question-admin-card { - margin-bottom: 14px; - padding: 16px; -} - -.q-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - margin-bottom: 10px; - font-size: 0.8rem; - flex-wrap: wrap; - gap: 8px; -} - -.highlight-answer { - color: var(--gold-bright); - font-weight: bold; - background: rgba(255, 215, 0, 0.1); - padding: 2px 6px; - border-radius: 4px; - word-break: break-word; -} - -.test-matcher-box { - margin-top: 12px; - padding: 10px; - background: rgba(0, 0, 0, 0.25); - border-radius: 6px; - border: 1px solid rgba(255, 255, 255, 0.1); -} - -.test-input-group { - display: flex; - gap: 6px; - margin-top: 6px; -} - -.test-result-feedback { - margin-top: 8px; - font-size: 0.85rem; - word-break: break-word; -} - -.q-actions { - display: flex; - gap: 8px; - margin-top: 14px; - flex-wrap: wrap; -} - -.inline-edit-form { - margin-top: 14px; - padding-top: 14px; - border-top: 1px solid rgba(255, 255, 255, 0.1); -} - -.empty-state { - text-align: center; - padding: 30px 10px; - color: var(--text-muted); -} - -/* ========================================= - FOOTER - ========================================= */ -.footer { - text-align: center; - padding: 16px; - color: var(--text-muted); - font-size: 0.75rem; - border-top: 1px solid rgba(255, 255, 255, 0.05); - margin-top: auto; -} - -.footer-links { - margin-top: 12px; - display: flex; - justify-content: center; - align-items: center; - gap: 12px; - flex-wrap: wrap; -} - -.footer-link { - color: var(--gold-soft); - text-decoration: none; - font-size: 0.82rem; - font-weight: 600; - padding: 4px 12px; - border-radius: 16px; - background: rgba(201, 168, 106, 0.12); - border: 1px solid rgba(201, 168, 106, 0.35); - transition: all 0.2s ease; - display: inline-flex; - align-items: center; - gap: 5px; -} - -.footer-link:hover { - color: #0b0b16; - background: var(--gold); - border-color: var(--gold-bright); - box-shadow: 0 0 12px rgba(201, 168, 106, 0.5); - text-decoration: none; - transform: translateY(-1px); -} - -.footer-sep { - display: none; -} - -.legal-card { - text-align: left; - max-width: 760px; - margin: 0 auto; -} - -.legal-content { - margin-top: 20px; - line-height: 1.65; - color: var(--text-main); -} - -.legal-content h3 { - color: var(--gold-soft); - margin-top: 22px; - margin-bottom: 8px; - font-size: 1.15rem; - border-bottom: 1px solid rgba(201, 168, 106, 0.2); - padding-bottom: 4px; -} - -.legal-content p, .legal-content ul { - margin-bottom: 14px; - color: var(--text-muted); - font-size: 0.95rem; -} - -.legal-content ul { - padding-left: 20px; -} - -.legal-content code { - background: rgba(0, 0, 0, 0.35); - padding: 2px 6px; - border-radius: 4px; - color: var(--gold-bright); - font-size: 0.88em; -} - -.hidden { - display: none !important; -} - -/* ========================================= - MOBILE RESPONSIVENESS (max-width: 640px) - ========================================= */ -@media (max-width: 640px) { - .container { - margin: 10px auto 25px; - padding: 0 10px; - } - - .admin-wrapper { - margin: 10px auto 25px; - padding: 0 10px; - } - - .card { - padding: 16px 12px; - border-radius: 12px; - } - - /* Mobile Hamburger Menu */ - .menu-toggle-btn { - display: inline-flex; - } - - .nav-links { - display: none; - position: absolute; - top: var(--header-height); - left: 0; - right: 0; - background: rgba(11, 11, 22, 0.98); - border-bottom: 2px solid var(--gold); - flex-direction: column; - padding: 14px 16px; - gap: 8px; - box-shadow: 0 15px 30px rgba(0,0,0,0.8); - } - - .nav-links.nav-open { - display: flex; - } - - .nav-link { - width: 100%; - padding: 12px 14px; - font-size: 1rem; - background: rgba(255, 255, 255, 0.04); - border: 1px solid rgba(201, 168, 106, 0.2); - } - - .audio-label { - display: none; /* Show only 🔇/🔊 on small phones */ - } - - .audio-btn { - padding: 6px 10px; - } - - /* Quiz Question layout */ - .choice-grid { - grid-template-columns: 1fr; /* Single column on mobile */ - gap: 10px; - } - - .choice-btn { - min-height: 48px; - padding: 10px 12px; - font-size: 0.95rem; - } - - .btn-group { - flex-direction: column; - } - - .btn-group .btn { - width: 100%; - } - - .test-input-group { - flex-direction: column; - } - - .test-input-group .btn { - width: 100%; - } - - .q-actions .btn { - flex: 1 1 100%; - } - - .admin-header { - flex-direction: column; - align-items: stretch; - text-align: center; - } - - .admin-top-actions { - justify-content: center; - } - - .lang-btn { - padding: 4px 6px; - font-size: 0.75rem; - } - - .diff-chip { - padding: 6px 10px; - font-size: 0.8rem; - } -} +:root { + --bg-dark: #070b14; + --bg-surface: #0f172a; + --bg-card: rgba(15, 23, 42, 0.78); + --bg-card-hover: rgba(30, 41, 59, 0.88); + --accent-primary: #38bdf8; + --accent-indigo: #6366f1; + --accent-violet: #8b5cf6; + --gold: #f59e0b; + --gold-bright: #fbbf24; + --gold-soft: #fcd34d; + --text-main: #f8fafc; + --text-muted: #94a3b8; + --success: #10b981; + --danger: #ef4444; + --warning: #f59e0b; + --border-subtle: rgba(148, 163, 184, 0.16); + --border-active: rgba(56, 189, 248, 0.55); + --border-gold: rgba(245, 158, 11, 0.35); + --header-height: 62px; +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; + -webkit-tap-highlight-color: transparent; +} + +body { + background-color: var(--bg-dark); + background-image: + radial-gradient(circle at 12% 18%, rgba(56, 189, 248, 0.10) 0%, transparent 42%), + radial-gradient(circle at 88% 15%, rgba(99, 102, 241, 0.11) 0%, transparent 45%), + radial-gradient(circle at 50% 90%, rgba(245, 158, 11, 0.06) 0%, transparent 50%); + background-attachment: fixed; + color: var(--text-main); + font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + min-height: 100vh; + display: flex; + flex-direction: column; + overflow-x: hidden; + line-height: 1.55; +} + +/* ========================================= + HEADER & NAVIGATION + ========================================= */ +.site-header { + background: rgba(7, 11, 20, 0.82); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border-bottom: 1px solid var(--border-subtle); + position: sticky; + top: 0; + z-index: 200; + width: 100%; +} + +.header-inner { + max-width: 960px; + margin: 0 auto; + padding: 0 18px; + height: var(--header-height); + display: flex; + justify-content: space-between; + align-items: center; + position: relative; +} + +.brand-link { + display: flex; + align-items: center; + gap: 10px; + text-decoration: none; + color: var(--text-main); + font-family: 'Space Grotesk', sans-serif; + font-weight: 700; + font-size: 1.25rem; + letter-spacing: -0.3px; +} + +.brand-badge { + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 9px; + background: linear-gradient(135deg, var(--accent-primary), var(--accent-indigo)); + color: #fff; + font-size: 1.05rem; + box-shadow: 0 4px 12px rgba(56, 189, 248, 0.3); +} + +.brand-accent { + color: var(--accent-primary); +} + +.header-right { + display: flex; + align-items: center; + gap: 10px; +} + +.lang-switch-group { + display: inline-flex; + background: rgba(15, 23, 42, 0.9); + border: 1px solid var(--border-subtle); + border-radius: 10px; + padding: 3px; +} + +.lang-btn { + background: transparent; + border: none; + color: var(--text-muted); + font-size: 0.78rem; + font-weight: 700; + padding: 5px 10px; + border-radius: 7px; + cursor: pointer; + transition: all 0.2s ease; + font-family: inherit; + display: flex; + align-items: center; + gap: 4px; +} + +.lang-btn:hover { + color: var(--text-main); +} + +.lang-btn.active { + background: rgba(56, 189, 248, 0.18); + color: var(--accent-primary); + border: 1px solid rgba(56, 189, 248, 0.35); +} + +.menu-toggle-btn { + display: none; + background: rgba(30, 41, 59, 0.6); + border: 1px solid var(--border-subtle); + color: var(--text-main); + font-size: 1.25rem; + width: 38px; + height: 38px; + border-radius: 9px; + cursor: pointer; + align-items: center; + justify-content: center; +} + +.nav-links { + display: flex; + gap: 6px; + align-items: center; +} + +.nav-link { + color: var(--text-muted); + text-decoration: none; + font-size: 0.88rem; + font-weight: 600; + padding: 8px 13px; + border-radius: 8px; + transition: all 0.2s ease; + white-space: nowrap; +} + +.nav-link:hover { + color: var(--text-main); + background: rgba(148, 163, 184, 0.1); +} + +/* ========================================= + CONTAINER & HERO HEADER + ========================================= */ +.container { + flex: 1; + max-width: 780px; + width: 100%; + margin: 28px auto; + padding: 0 16px; +} + +.admin-wrapper { + max-width: 940px; + width: 100%; + margin: 24px auto; + padding: 0 16px; +} + +.quiz-wrapper { + width: 100%; +} + +.quiz-header { + text-align: center; + margin-bottom: 24px; +} + +.hero-pill { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 5px 14px; + border-radius: 999px; + background: rgba(56, 189, 248, 0.1); + border: 1px solid rgba(56, 189, 248, 0.28); + color: var(--accent-primary); + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.3px; + margin-bottom: 12px; +} + +.glow-title { + font-family: 'Space Grotesk', sans-serif; + color: var(--text-main); + font-size: clamp(1.65rem, 5vw, 2.35rem); + font-weight: 700; + letter-spacing: -0.6px; + margin-bottom: 8px; + word-break: break-word; +} + +.subtitle { + color: var(--text-muted); + font-size: clamp(0.9rem, 3vw, 1.02rem); + max-width: 580px; + margin: 0 auto 12px; +} + +/* ========================================= + CARD & DIRECT-START DASHBOARD + ========================================= */ +.card { + background: var(--bg-card); + border: 1px solid var(--border-subtle); + border-radius: 18px; + padding: clamp(18px, 4vw, 30px); + box-shadow: 0 20px 50px rgba(0, 0, 0, 0.45), inset 0 1px 0 rgba(255, 255, 255, 0.05); + backdrop-filter: blur(18px); + -webkit-backdrop-filter: blur(18px); + margin-bottom: 20px; + width: 100%; +} + +.start-dashboard-card { + padding: clamp(20px, 4.5vw, 32px); +} + +.dashboard-grid { + display: flex; + flex-direction: column; + gap: 24px; +} + +.config-section { + display: flex; + flex-direction: column; + gap: 12px; +} + +.config-header { + display: flex; + align-items: center; + gap: 10px; +} + +.config-step { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 6px; + background: rgba(99, 102, 241, 0.2); + border: 1px solid rgba(99, 102, 241, 0.4); + color: #a5b4fc; + font-family: 'Space Grotesk', sans-serif; + font-size: 0.78rem; + font-weight: 700; +} + +.difficulty-label { + font-size: 0.92rem; + color: var(--text-main); + font-weight: 700; + letter-spacing: 0.2px; +} + +.difficulty-chips { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 10px; +} + +.diff-chip { + background: rgba(30, 41, 59, 0.55); + border: 1px solid var(--border-subtle); + color: var(--text-muted); + border-radius: 12px; + padding: 11px 12px; + font-size: 0.88rem; + font-weight: 600; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 7px; + transition: all 0.18s ease; + font-family: inherit; +} + +.diff-chip:hover { + border-color: rgba(148, 163, 184, 0.4); + color: var(--text-main); + background: rgba(30, 41, 59, 0.85); + transform: translateY(-1px); +} + +.diff-chip.active { + background: rgba(56, 189, 248, 0.15); + border-color: var(--accent-primary); + color: #fff; + box-shadow: 0 0 18px rgba(56, 189, 248, 0.22); + font-weight: 700; +} + +/* Category Selector Chips */ +.category-chips { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 10px; +} + +.cat-chip { + background: rgba(30, 41, 59, 0.5); + border: 1px solid var(--border-subtle); + color: var(--text-muted); + border-radius: 12px; + padding: 11px 12px; + font-size: 0.85rem; + font-weight: 600; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 8px; + transition: all 0.18s ease; + font-family: inherit; + text-align: left; +} + +.cat-chip:hover { + border-color: rgba(148, 163, 184, 0.4); + color: var(--text-main); + background: rgba(30, 41, 59, 0.85); + transform: translateY(-1px); +} + +.cat-chip.active { + background: linear-gradient(135deg, rgba(99, 102, 241, 0.22), rgba(56, 189, 248, 0.18)); + border-color: var(--accent-primary); + color: #fff; + box-shadow: 0 0 16px rgba(56, 189, 248, 0.2); + font-weight: 700; +} + +.dashboard-footer { + margin-top: 26px; + padding-top: 20px; + border-top: 1px solid var(--border-subtle); + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; +} + +.dashboard-rules { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 10px; + font-size: 0.82rem; + color: var(--text-muted); +} + +/* ========================================= + BUTTONS + ========================================= */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 12px 22px; + border-radius: 12px; + font-size: 0.95rem; + font-weight: 700; + cursor: pointer; + text-decoration: none; + transition: all 0.2s ease; + border: none; + font-family: inherit; + min-height: 46px; + touch-action: manipulation; +} + +.btn-primary { + background: linear-gradient(135deg, var(--accent-primary) 0%, var(--accent-indigo) 100%); + color: #fff; + box-shadow: 0 6px 20px rgba(56, 189, 248, 0.28); +} + +.btn-primary:hover:not(:disabled) { + box-shadow: 0 8px 26px rgba(56, 189, 248, 0.45); + transform: translateY(-2px); + filter: brightness(1.06); +} + +.btn-launch-quiz { + width: 100%; + max-width: 400px; + font-size: 1.06rem; + padding: 15px 28px; + border-radius: 14px; + letter-spacing: 0.2px; +} + +.btn-secondary { + background: rgba(30, 41, 59, 0.75); + color: var(--text-main); + border: 1px solid var(--border-subtle); +} + +.btn-secondary:hover:not(:disabled) { + background: rgba(51, 65, 85, 0.85); + border-color: rgba(148, 163, 184, 0.4); +} + +.btn-danger { + background: rgba(239, 68, 68, 0.15); + color: #fca5a5; + border: 1px solid rgba(239, 68, 68, 0.4); +} + +.btn-danger:hover:not(:disabled) { + background: var(--danger); + color: #fff; +} + +.btn-success { + background: rgba(16, 185, 129, 0.18); + color: #6ee7b7; + border: 1px solid rgba(16, 185, 129, 0.4); +} + +.btn-success:hover:not(:disabled) { + background: var(--success); + color: #06281e; +} + +.btn-large { + font-size: 1.05rem; + padding: 14px 28px; + min-height: 52px; +} + +.btn-sm { + font-size: 0.84rem; + padding: 8px 14px; + min-height: 36px; +} + +.btn-group { + display: flex; + gap: 10px; + justify-content: center; + margin-top: 18px; + width: 100%; +} + +.btn:disabled { + opacity: 0.55; + cursor: not-allowed; + transform: none !important; +} + +/* ========================================= + HUD STATUS, STREAK & PROGRESS BAR + ========================================= */ +.status-panel { + margin-bottom: 16px; + width: 100%; +} + +.progress-info { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; + gap: 8px; + flex-wrap: wrap; +} + +.hud-pill { + font-family: 'Space Grotesk', sans-serif; + font-size: 0.88rem; + font-weight: 700; + color: var(--text-muted); + background: rgba(15, 23, 42, 0.8); + border: 1px solid var(--border-subtle); + padding: 5px 12px; + border-radius: 10px; +} + +.hud-center-group { + display: flex; + align-items: center; + gap: 8px; +} + +.streak-badge { + font-family: 'Space Grotesk', sans-serif; + font-size: 0.85rem; + font-weight: 700; + padding: 5px 12px; + border-radius: 10px; + background: rgba(30, 41, 59, 0.75); + border: 1px solid var(--border-subtle); + color: var(--text-muted); + transition: all 0.25s ease; +} + +.streak-badge.streak-hot { + background: rgba(245, 158, 11, 0.16); + border-color: rgba(245, 158, 11, 0.5); + color: var(--gold-bright); + box-shadow: 0 0 14px rgba(245, 158, 11, 0.25); +} + +.streak-pop { + animation: streakPopAnim 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +@keyframes streakPopAnim { + 0% { transform: scale(1); } + 50% { transform: scale(1.22); } + 100% { transform: scale(1); } +} + +.timer-badge { + font-family: 'Space Grotesk', ui-monospace, monospace; + font-size: 0.85rem; + font-weight: 700; + padding: 5px 12px; + border-radius: 10px; + background: rgba(15, 23, 42, 0.8); + border: 1px solid var(--border-subtle); + color: var(--text-main); +} + +.points-badge { + font-family: 'Space Grotesk', sans-serif; + font-size: 0.9rem; + font-weight: 700; + padding: 5px 13px; + border-radius: 10px; + background: rgba(56, 189, 248, 0.14); + border: 1px solid rgba(56, 189, 248, 0.35); + color: var(--accent-primary); +} + +.progress-container { + width: 100%; + height: 8px; + background: rgba(30, 41, 59, 0.8); + border-radius: 999px; + overflow: hidden; +} + +.progress-bar { + height: 100%; + width: 0%; + background: linear-gradient(90deg, var(--accent-primary) 0%, var(--accent-indigo) 100%); + box-shadow: 0 0 12px rgba(56, 189, 248, 0.5); + transition: width 0.35s ease; +} + +/* ========================================= + QUESTION CARD & ANSWERS + ========================================= */ +.question-card { + text-align: center; +} + +.question-meta { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 18px; + gap: 8px; +} + +.meta-left { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +.badge { + padding: 5px 11px; + border-radius: 8px; + font-size: 0.76rem; + font-weight: 700; + background: rgba(99, 102, 241, 0.15); + border: 1px solid rgba(99, 102, 241, 0.35); + color: #c7d2fe; + white-space: nowrap; +} + +.badge-diff { + background: rgba(255, 255, 255, 0.06); + border: 1px solid rgba(255, 255, 255, 0.16); + color: var(--text-main); +} + +.badge-diff-easy { + background: rgba(16, 185, 129, 0.14); + border-color: rgba(16, 185, 129, 0.4); + color: #34d399; +} + +.badge-diff-medium { + background: rgba(245, 158, 11, 0.14); + border-color: rgba(245, 158, 11, 0.4); + color: #fbbf24; +} + +.badge-diff-hard { + background: rgba(239, 68, 68, 0.14); + border-color: rgba(239, 68, 68, 0.4); + color: #f87171; +} + +.badge-warning { + background: rgba(245, 158, 11, 0.2); + border-color: var(--warning); + color: #fbbf24; +} + +.badge-success { + background: rgba(16, 185, 129, 0.2); + border-color: var(--success); + color: #34d399; +} + +.badge-danger { + background: rgba(239, 68, 68, 0.2); + border-color: var(--danger); + color: #f87171; +} + +.badge-muted { + background: rgba(148, 163, 184, 0.12); + border-color: rgba(148, 163, 184, 0.25); + color: var(--text-muted); +} + +.attempt-badge { + font-size: 0.8rem; + font-weight: 600; + color: var(--text-muted); + white-space: nowrap; +} + +.question-text { + font-family: 'Space Grotesk', sans-serif; + font-size: clamp(1.2rem, 4.2vw, 1.5rem); + line-height: 1.42; + text-align: center; + margin: 14px 0 24px; + color: var(--text-main); + font-weight: 700; + word-break: break-word; +} + +/* Free-Text Input */ +.answer-form { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; +} + +.input-answer { + width: 100%; + max-width: 480px; + padding: 14px 18px; + font-size: 16px; + text-align: center; + border-radius: 12px; + border: 1.5px solid var(--border-subtle); + background: rgba(7, 11, 20, 0.65); + color: var(--text-main); + outline: none; + transition: all 0.2s ease; + font-family: inherit; + min-height: 50px; +} + +.input-answer:focus { + border-color: var(--accent-primary); + box-shadow: 0 0 0 3px rgba(56, 189, 248, 0.18); +} + +/* Multiple Choice Grid */ +.choice-group { + width: 100%; + margin-top: 4px; +} + +.choice-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 12px; + width: 100%; +} + +.choice-btn { + background: rgba(30, 41, 59, 0.55); + border: 1.5px solid var(--border-subtle); + border-radius: 13px; + padding: 13px 15px; + color: var(--text-main); + font-size: clamp(0.92rem, 3.2vw, 1.0rem); + font-weight: 600; + font-family: inherit; + cursor: pointer; + text-align: left; + display: flex; + align-items: center; + gap: 12px; + transition: all 0.18s ease; + min-height: 56px; + width: 100%; + word-break: break-word; + touch-action: manipulation; +} + +.choice-btn:hover:not(:disabled) { + background: rgba(56, 189, 248, 0.12); + border-color: var(--accent-primary); + transform: translateY(-2px); +} + +.choice-btn:active:not(:disabled) { + transform: scale(0.99); +} + +.choice-letter { + display: inline-flex; + align-items: center; + justify-content: center; + width: 30px; + height: 30px; + background: rgba(56, 189, 248, 0.14); + border: 1px solid rgba(56, 189, 248, 0.35); + border-radius: 8px; + color: var(--accent-primary); + font-family: 'Space Grotesk', sans-serif; + font-weight: 700; + font-size: 0.85rem; + flex-shrink: 0; +} + +.choice-text { + flex: 1; +} + +.choice-selected { + border-color: var(--accent-primary); + background: rgba(56, 189, 248, 0.22); +} + +.choice-correct { + background: rgba(16, 185, 129, 0.22) !important; + border-color: #10b981 !important; + color: #6ee7b7 !important; + box-shadow: 0 0 16px rgba(16, 185, 129, 0.35) !important; +} + +.choice-wrong { + background: rgba(239, 68, 68, 0.22) !important; + border-color: #ef4444 !important; + color: #fca5a5 !important; + box-shadow: 0 0 16px rgba(239, 68, 68, 0.3) !important; +} + +.hint-box { + margin-top: 16px; + padding: 13px 16px; + border-radius: 12px; + background: rgba(245, 158, 11, 0.1); + border-left: 4px solid var(--gold); + font-size: 0.9rem; + color: var(--gold-soft); + text-align: left; + word-break: break-word; +} + +.feedback-box { + margin-top: 16px; + font-size: clamp(0.95rem, 3.5vw, 1.08rem); + text-align: center; + min-height: 28px; + font-weight: 700; + word-break: break-word; +} + +.feedback-correct { + color: #34d399; +} + +.feedback-wrong { + color: #f87171; +} + +.feedback-revealed { + color: var(--gold-bright); + font-size: 0.92rem; + margin-top: 6px; +} + +/* ========================================= + ANIMATIONS + ========================================= */ +.sparkle-effect { + animation: sparkle-glow 0.7s ease forwards; +} + +@keyframes sparkle-glow { + 0% { box-shadow: 0 20px 50px rgba(0, 0, 0, 0.45); } + 50% { box-shadow: 0 0 30px rgba(16, 185, 129, 0.45); border-color: #10b981; } + 100% { box-shadow: 0 20px 50px rgba(0, 0, 0, 0.45); } +} + +.shake-effect { + animation: shake 0.4s ease; +} + +@keyframes shake { + 0%, 100% { transform: translateX(0); } + 20%, 60% { transform: translateX(-6px); } + 40%, 80% { transform: translateX(6px); } +} + +/* ========================================= + FINALE & LEADERBOARD + ========================================= */ +.finale-card { + text-align: center; +} + +.finale-header { + display: flex; + justify-content: center; + align-items: center; + gap: 10px; + margin-bottom: 16px; +} + +.sparkle-icon { + font-size: 1.5rem; +} + +.score-banner { + background: rgba(30, 41, 59, 0.6); + border: 1px solid var(--border-subtle); + border-radius: 14px; + padding: 18px; + margin-bottom: 20px; +} + +.score-highlight { + font-size: clamp(1.05rem, 4vw, 1.28rem); +} + +.gold-text { + color: var(--accent-primary); + font-family: 'Space Grotesk', sans-serif; + font-weight: 700; + font-size: 1.3rem; +} + +.reward-box { + background: rgba(15, 23, 42, 0.7); + border-radius: 14px; + padding: 20px 18px; + margin-bottom: 20px; + border-left: 4px solid var(--accent-primary); + text-align: left; +} + +.reward-box h2 { + color: var(--text-main); + font-size: clamp(1.15rem, 4vw, 1.4rem); + margin-bottom: 10px; +} + +.reward-description { + font-size: clamp(0.94rem, 3.3vw, 1.02rem); + line-height: 1.6; + color: var(--text-muted); +} + +.time-highlight { + margin-top: 8px; + font-size: 0.95rem; + color: var(--text-muted); +} + +.tier-box { + position: relative; + border-radius: 16px; + padding: 24px 20px; + margin: 20px 0; + border: 1px solid var(--border-subtle); + background: radial-gradient(circle at top, rgba(56, 189, 248, 0.1) 0%, rgba(15, 23, 42, 0.85) 100%); + text-align: center; +} + +.tier-badge-container { + display: flex; + justify-content: center; + margin-bottom: 14px; +} + +.tier-grade-badge { + display: inline-flex; + align-items: center; + justify-content: center; + width: 64px; + height: 64px; + border-radius: 16px; + font-family: 'Space Grotesk', sans-serif; + font-size: 2rem; + font-weight: 800; + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4); + border: 2px solid; +} + +.tier-outstanding { + background: linear-gradient(135deg, #fbbf24, #d97706); + color: #090d16; + border-color: #fde68a; +} + +.tier-exceeds { + background: linear-gradient(135deg, #34d399, #059669); + color: #090d16; + border-color: #a7f3d0; +} + +.tier-acceptable { + background: linear-gradient(135deg, #38bdf8, #2563eb); + color: #ffffff; + border-color: #bae6fd; +} + +.tier-poor { + background: linear-gradient(135deg, #fb923c, #ea580c); + color: #ffffff; + border-color: #fed7aa; +} + +.tier-troll { + background: linear-gradient(135deg, #64748b, #334155); + color: #fca5a5; + border-color: #ef4444; +} + +.tier-subtitle { + color: var(--accent-primary); + font-weight: 600; + margin-bottom: 10px; + font-size: 0.95rem; +} + +.leaderboard-entry-card { + background: rgba(15, 23, 42, 0.7); + border: 1px solid var(--border-subtle); + padding: 22px; + margin-top: 20px; + text-align: center; +} + +.input-with-btn { + display: flex; + gap: 10px; + max-width: 500px; + margin: 0 auto; + width: 100%; +} + +.input-with-btn .input-text { + flex: 1; + text-align: left; +} + +.btn-dice { + background: rgba(30, 41, 59, 0.8); + border: 1px solid var(--border-subtle); + color: var(--text-main); + padding: 0 14px; + border-radius: 10px; + cursor: pointer; + font-size: 0.9rem; + font-family: inherit; + font-weight: 700; + display: inline-flex; + align-items: center; + gap: 6px; + justify-content: center; + transition: all 0.2s ease; + min-height: 46px; + white-space: nowrap; +} + +.btn-dice:hover { + background: rgba(56, 189, 248, 0.15); + border-color: var(--accent-primary); + color: var(--accent-primary); +} + +.diff-filter-tabs { + display: flex; + flex-wrap: wrap; + gap: 8px; + justify-content: center; + margin: 16px 0; +} + +.diff-filter-tabs .tab-btn { + border-radius: 10px; + padding: 8px 16px; + font-size: 0.86rem; + background: rgba(30, 41, 59, 0.6); + border: 1px solid var(--border-subtle); + color: var(--text-muted); + cursor: pointer; + font-weight: 600; +} + +.diff-filter-tabs .tab-btn.active { + background: rgba(56, 189, 248, 0.16); + border-color: var(--accent-primary); + color: #fff; +} + +.table-responsive { + width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} + +.lb-table { + width: 100%; + border-collapse: collapse; + text-align: left; + font-size: 0.9rem; +} + +.lb-table th { + background: rgba(30, 41, 59, 0.7); + color: var(--text-muted); + font-weight: 700; + padding: 11px 12px; + border-bottom: 1px solid var(--border-subtle); + white-space: nowrap; +} + +.lb-table td { + padding: 11px 12px; + border-bottom: 1px solid rgba(148, 163, 184, 0.1); + color: var(--text-main); +} + +.lb-table tr:hover td { + background: rgba(56, 189, 248, 0.05); +} + +/* ========================================= + FORMS & INPUTS + ========================================= */ +.magic-form { + display: flex; + flex-direction: column; + gap: 16px; + text-align: left; + width: 100%; +} + +.form-group { + display: flex; + flex-direction: column; + gap: 6px; + width: 100%; +} + +.form-group label { + font-size: 0.88rem; + font-weight: 700; + color: var(--text-main); +} + +.input-text, .textarea, select.input-text { + padding: 12px 14px; + border-radius: 10px; + border: 1px solid var(--border-subtle); + background: rgba(7, 11, 20, 0.65); + color: var(--text-main); + font-size: 16px; + font-family: inherit; + outline: none; + width: 100%; + min-height: 46px; +} + +.input-text:focus, .textarea:focus, select.input-text:focus { + border-color: var(--accent-primary); + box-shadow: 0 0 0 3px rgba(56, 189, 248, 0.16); +} + +.input-text-sm { + padding: 8px 10px; + border-radius: 8px; + border: 1px solid var(--border-subtle); + background: rgba(7, 11, 20, 0.55); + color: var(--text-main); + font-size: 15px; + min-height: 38px; + width: 100%; +} + +.field-help { + font-size: 0.8rem; + color: var(--text-muted); +} + +.info-alert { + background: rgba(56, 189, 248, 0.1); + border-left: 4px solid var(--accent-primary); + padding: 14px; + border-radius: 10px; + margin-bottom: 20px; + font-size: 0.88rem; + line-height: 1.5; + color: var(--text-main); +} + +.alert-danger { + background: rgba(239, 68, 68, 0.15); + border: 1px solid var(--danger); + color: #fca5a5; + padding: 12px; + border-radius: 10px; + margin-bottom: 16px; + text-align: center; + font-size: 0.9rem; +} + +/* ========================================= + ADMIN COCKPIT + ========================================= */ +.admin-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + flex-wrap: wrap; + gap: 12px; +} + +.admin-top-actions { + display: flex; + gap: 8px; +} + +.tabs-nav { + display: flex; + gap: 6px; + border-bottom: 1px solid var(--border-subtle); + margin-bottom: 20px; + overflow-x: auto; + scrollbar-width: none; + padding-bottom: 2px; +} + +.tabs-nav::-webkit-scrollbar { + display: none; +} + +.tab-btn { + background: rgba(30, 41, 59, 0.5); + border: 1px solid transparent; + color: var(--text-muted); + padding: 10px 14px; + border-radius: 10px 10px 0 0; + cursor: pointer; + font-size: 0.85rem; + font-family: inherit; + font-weight: 700; + display: inline-flex; + align-items: center; + gap: 6px; + white-space: nowrap; + flex-shrink: 0; + min-height: 42px; +} + +.tab-btn.active { + background: var(--bg-card); + color: var(--accent-primary); + border-color: var(--border-subtle); + border-bottom-color: transparent; +} + +.tab-content { + display: none; +} + +.tab-content.active { + display: block; +} + +.question-admin-card { + margin-bottom: 14px; + padding: 18px; +} + +.q-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 10px; + font-size: 0.8rem; + flex-wrap: wrap; + gap: 8px; +} + +.highlight-answer { + color: var(--accent-primary); + font-weight: 700; + background: rgba(56, 189, 248, 0.1); + padding: 2px 8px; + border-radius: 6px; + word-break: break-word; +} + +.test-matcher-box { + margin-top: 12px; + padding: 12px; + background: rgba(7, 11, 20, 0.45); + border-radius: 10px; + border: 1px solid var(--border-subtle); +} + +.test-input-group { + display: flex; + gap: 8px; + margin-top: 6px; +} + +.test-result-feedback { + margin-top: 8px; + font-size: 0.85rem; + word-break: break-word; +} + +.q-actions { + display: flex; + gap: 8px; + margin-top: 14px; + flex-wrap: wrap; +} + +.inline-edit-form { + margin-top: 14px; + padding-top: 14px; + border-top: 1px solid var(--border-subtle); +} + +.empty-state { + text-align: center; + padding: 30px 10px; + color: var(--text-muted); +} + +/* ========================================= + FOOTER & LEGAL + ========================================= */ +.footer { + text-align: center; + padding: 20px 16px; + color: var(--text-muted); + font-size: 0.8rem; + border-top: 1px solid var(--border-subtle); + margin-top: auto; +} + +.footer-links { + margin-top: 10px; + display: flex; + justify-content: center; + align-items: center; + gap: 10px; + flex-wrap: wrap; +} + +.footer-link { + color: var(--text-muted); + text-decoration: none; + font-size: 0.8rem; + font-weight: 600; + padding: 4px 12px; + border-radius: 8px; + background: rgba(30, 41, 59, 0.5); + border: 1px solid var(--border-subtle); + transition: all 0.2s ease; +} + +.footer-link:hover { + color: var(--text-main); + border-color: var(--accent-primary); +} + +.footer-sep { + display: none; +} + +.legal-card { + text-align: left; + max-width: 760px; + margin: 0 auto; +} + +.legal-content { + margin-top: 20px; + line-height: 1.65; + color: var(--text-main); +} + +.legal-content h3 { + color: var(--accent-primary); + margin-top: 22px; + margin-bottom: 8px; + font-size: 1.15rem; + border-bottom: 1px solid var(--border-subtle); + padding-bottom: 4px; +} + +.legal-content p, .legal-content ul { + margin-bottom: 14px; + color: var(--text-muted); + font-size: 0.95rem; +} + +.legal-content ul { + padding-left: 20px; +} + +.legal-content code { + background: rgba(0, 0, 0, 0.35); + padding: 2px 6px; + border-radius: 4px; + color: var(--accent-primary); + font-size: 0.88em; +} + +.hidden { + display: none !important; +} + +/* ========================================= + MOBILE RESPONSIVENESS + ========================================= */ +@media (max-width: 680px) { + .container { + margin: 14px auto 25px; + padding: 0 12px; + } + + .card { + padding: 18px 14px; + border-radius: 14px; + } + + .menu-toggle-btn { + display: inline-flex; + } + + .nav-links { + display: none; + position: absolute; + top: var(--header-height); + left: 0; + right: 0; + background: rgba(7, 11, 20, 0.98); + border-bottom: 1px solid var(--border-subtle); + flex-direction: column; + padding: 14px 16px; + gap: 8px; + box-shadow: 0 16px 32px rgba(0, 0, 0, 0.75); + } + + .nav-links.nav-open { + display: flex; + } + + .nav-link { + width: 100%; + padding: 12px 14px; + font-size: 0.96rem; + background: rgba(30, 41, 59, 0.5); + border: 1px solid var(--border-subtle); + } + + .difficulty-chips { + grid-template-columns: repeat(2, 1fr); + } + + .category-chips { + grid-template-columns: repeat(2, 1fr); + } + + .choice-grid { + grid-template-columns: 1fr; + gap: 10px; + } + + .btn-group { + flex-direction: column; + } + + .btn-group .btn { + width: 100%; + } + + .input-with-btn { + flex-direction: column; + } + + .test-input-group { + flex-direction: column; + } +} + +@media (max-width: 420px) { + .category-chips { + grid-template-columns: 1fr; + } +} diff --git a/app/static/js/quiz.js b/app/static/js/quiz.js index 0c68e8d..5d6bab2 100755 --- a/app/static/js/quiz.js +++ b/app/static/js/quiz.js @@ -1,586 +1,606 @@ -let currentSessionId = null; -let currentQuestionNumber = 1; -let totalQuestions = 10; -let currentScore = 0; -let attemptsUsed = 0; -const maxAttempts = 3; -let selectedDifficulty = "all"; -let lastQuestionData = null; -let timerStartTime = null; -let timerInterval = null; -let elapsedSeconds = 0; - -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", - envelopeRecipient: "An den ehrenwerten Zauberschüler", - envelopeLocation: "Auf dem Prüfungspfad • Schloss Hogwarts", - letterSchoolTitle: "Hogwarts-Schule für Hexerei und Zauberei", - letterHeadmaster: "Schulleiter: Albus Dumbledore
(Großhexenmeister, Träger des Merlin-Ordens Erster Klasse)", - letterSalutation: "Liebe Hexe, lieber Zauberer,", - letterParagraph1: "wir freuen uns, dir mitteilen zu dürfen, dass du zu den Magischen Z.A.G.-Prüfungen des Zaubereiministeriums zugelassen bist.", - letterParagraph2: "Zehn anspruchsvolle Prüfungsfragen entscheiden über deinen Rang in den Chroniken von Hogwarts. Zeige deinen Scharfsinn und sichere bis zu 1.000 Hauspunkte für dein Haus!", - letterClosing: "Mit vorzüglicher Hochachtung,", - letterSignature: "Minerva McGonagall", - letterSignTitle: "Stellvertretende Schulleiterin", - introDesc: "Ein verzauberter Pergamentbrief aus Hogwarts liegt vor dir.
Wähle deinen Schwierigkeitsgrad und brich das rote Zaubersiegel:", - 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: "✨ Antwort prüfen", - hintBtn: "💡 Hinweis anzeigen", - 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", - envelopeRecipient: "To the Esteemed Wizarding Student", - envelopeLocation: "On the Path of Trials • Hogwarts Castle", - letterSchoolTitle: "Hogwarts School of Witchcraft and Wizardry", - letterHeadmaster: "Headmaster: Albus Dumbledore
(Order of Merlin, First Class, Grand Sorc.)", - letterSalutation: "Dear Witch or Wizard,", - letterParagraph1: "we are pleased to inform you that you have been admitted to the Magical O.W.L. Examinations of the Ministry of Magic.", - letterParagraph2: "Ten demanding examination questions will determine your rank in the Hogwarts archives. Show your wisdom and claim up to 1,000 House Points for your house!", - letterClosing: "Yours sincerely,", - letterSignature: "Minerva McGonagall", - letterSignTitle: "Deputy Headmistress", - introDesc: "An enchanted Hogwarts parchment letter lies before you.
Choose your difficulty and break the crimson magical seal:", - 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 Answer", - hintBtn: "💡 Show 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 -const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); - -function playSound(type) { - if (audioCtx.state === 'suspended') { - audioCtx.resume(); - } - if (type === 'magic') { - // Magical bright arpeggio chime - const notes = [523.25, 659.25, 783.99, 1046.50]; // C5, E5, G5, C6 - notes.forEach((freq, index) => { - const osc = audioCtx.createOscillator(); - const gain = audioCtx.createGain(); - osc.type = 'sine'; - osc.frequency.value = freq; - gain.gain.setValueAtTime(0.15, audioCtx.currentTime + index * 0.08); - gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + index * 0.08 + 0.35); - osc.connect(gain); - gain.connect(audioCtx.destination); - osc.start(audioCtx.currentTime + index * 0.08); - osc.stop(audioCtx.currentTime + index * 0.08 + 0.35); - }); - } else if (type === 'wrong') { - // Soft error buzz - const osc = audioCtx.createOscillator(); - const gain = audioCtx.createGain(); - osc.type = 'sawtooth'; - osc.frequency.setValueAtTime(150, audioCtx.currentTime); - osc.frequency.exponentialRampToValueAtTime(80, audioCtx.currentTime + 0.25); - gain.gain.setValueAtTime(0.1, audioCtx.currentTime); - gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.25); - osc.connect(gain); - gain.connect(audioCtx.destination); - osc.start(); - osc.stop(audioCtx.currentTime + 0.25); - } -} - -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"); - - const letterSchoolTitle = document.getElementById("letterSchoolTitle"); - const letterHeadmaster = document.getElementById("letterHeadmaster"); - const letterSalutation = document.getElementById("letterSalutation"); - const letterParagraph1 = document.getElementById("letterParagraph1"); - const letterParagraph2 = document.getElementById("letterParagraph2"); - const letterClosing = document.getElementById("letterClosing"); - const letterSignature = document.getElementById("letterSignature"); - const letterSignTitle = document.getElementById("letterSignTitle"); - - const envelopeRecipient = document.getElementById("envelopeRecipient"); - const envelopeLocation = document.getElementById("envelopeLocation"); - - if (pageTitle) pageTitle.innerText = t.pageTitle; - if (pageSubtitle) pageSubtitle.innerText = t.pageSubtitle; - if (envelopeLetter) envelopeLetter.innerText = t.envelopeLetter; - if (envelopeRecipient && t.envelopeRecipient) envelopeRecipient.innerText = t.envelopeRecipient; - if (envelopeLocation && t.envelopeLocation) envelopeLocation.innerText = t.envelopeLocation; - if (letterSchoolTitle && t.letterSchoolTitle) letterSchoolTitle.innerText = t.letterSchoolTitle; - if (letterHeadmaster && t.letterHeadmaster) letterHeadmaster.innerHTML = t.letterHeadmaster; - if (letterSalutation && t.letterSalutation) letterSalutation.innerText = t.letterSalutation; - if (letterParagraph1 && t.letterParagraph1) letterParagraph1.innerHTML = t.letterParagraph1; - if (letterParagraph2 && t.letterParagraph2) letterParagraph2.innerHTML = t.letterParagraph2; - if (letterClosing && t.letterClosing) letterClosing.innerText = t.letterClosing; - if (letterSignature && t.letterSignature) letterSignature.innerText = t.letterSignature; - if (letterSignTitle && t.letterSignTitle) letterSignTitle.innerText = t.letterSignTitle; - 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); - } -} - -let sessionQuestionIds = []; - -function getRecentQuestionIds() { - try { - const stored = localStorage.getItem("quiz_recent_qids"); - return stored ? JSON.parse(stored) : []; - } catch(e) { - return []; - } -} - -function getWrongQuestionIds() { - try { - const stored = localStorage.getItem("quiz_wrong_qids"); - return stored ? JSON.parse(stored) : []; - } catch(e) { - return []; - } -} - -function recordQuestionWrong(qid) { - if (!qid) return; - try { - const wrong = getWrongQuestionIds(); - if (!wrong.includes(qid)) { - wrong.push(qid); - if (wrong.length > 30) wrong.shift(); - localStorage.setItem("quiz_wrong_qids", JSON.stringify(wrong)); - } - } catch(e) {} -} - -function recordQuestionMastered(qid) { - if (!qid) return; - try { - let wrong = getWrongQuestionIds(); - if (wrong.includes(qid)) { - wrong = wrong.filter(id => id !== qid); - localStorage.setItem("quiz_wrong_qids", JSON.stringify(wrong)); - } - } catch(e) {} -} - -function saveRecentQuestions(ids) { - if (!ids || ids.length === 0) return; - try { - let recent = getRecentQuestionIds(); - recent = Array.from(new Set([...ids, ...recent])).slice(0, 20); - localStorage.setItem("quiz_recent_qids", JSON.stringify(recent)); - } catch(e) {} -} - -async function startGame() { - const envelope = document.getElementById("envelope"); - const startBtn = document.getElementById("startBtn"); - const t = UI_TEXT[currentLang] || UI_TEXT.de; - - startBtn.disabled = true; - startBtn.innerText = t.startBtnCasting; - envelope.classList.add("envelope-opened"); - playSound('magic'); - sessionQuestionIds = []; - - // Start background music if enabled or first click - const bgm = document.getElementById("bgm"); - if (bgm && bgm.paused) { - bgm.play().then(() => { - const icon = document.getElementById("audioIcon"); - const text = document.getElementById("audioText"); - if (icon && text) { - icon.innerText = "🔊"; - text.innerText = (currentLang === "de") ? "Musik: An" : "Music: On"; - } - localStorage.setItem("bgm_enabled", "true"); - }).catch(() => {}); - } - - try { - const recentIds = getRecentQuestionIds(); - const wrongIds = getWrongQuestionIds(); - - const res = await fetch("/api/game/start", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - lang: currentLang, - difficulty: selectedDifficulty, - recent_ids: recentIds, - wrong_ids: wrongIds - }) - }); - - if (!res.ok) { - throw new Error(currentLang === "en" ? "Could not start game session." : "Konnte Spiel nicht starten."); - } - - const data = await res.json(); - currentSessionId = data.session_id; - currentQuestionNumber = data.question_number; - totalQuestions = data.total_questions; - currentScore = data.score; - attemptsUsed = data.attempts_used; - - setTimeout(() => { - document.getElementById("introCard").classList.add("hidden"); - document.getElementById("gameCard").classList.remove("hidden"); - startTimer(); - renderQuestion(data); - }, 1800); - - } catch (err) { - alert(t.startError + err.message); - startBtn.disabled = false; - startBtn.innerText = t.startBtn; - envelope.classList.remove("envelope-opened"); - } -} - -function startTimer() { - timerStartTime = Date.now(); - elapsedSeconds = 0; - if (timerInterval) clearInterval(timerInterval); - timerInterval = setInterval(updateTimer, 1000); - updateTimer(); -} - -function updateTimer() { - if (!timerStartTime) return; - elapsedSeconds = Math.floor((Date.now() - timerStartTime) / 1000); - const m = Math.floor(elapsedSeconds / 60); - const s = elapsedSeconds % 60; - const badge = document.getElementById("timerBadge"); - if (badge) { - badge.innerText = `⏱️ ${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`; - } -} - -function renderQuestion(qData) { - lastQuestionData = qData; - if (qData.id && !sessionQuestionIds.includes(qData.id)) { - sessionQuestionIds.push(qData.id); - } - 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()}`; - - // 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; - - const textGroup = document.getElementById("textAnswerGroup"); - const choiceGroup = document.getElementById("choiceAnswerGroup"); - const choiceContainer = document.getElementById("choiceOptionsContainer"); - const input = document.getElementById("answerInput"); - - document.getElementById("hintBox").classList.add("hidden"); - document.getElementById("feedbackBox").innerHTML = ""; - - if (qData.question_type === "multiple_choice" && qData.options && qData.options.length > 0) { - textGroup.classList.add("hidden"); - choiceGroup.classList.remove("hidden"); - choiceContainer.innerHTML = ""; - - const letters = ["A", "B", "C", "D", "E", "F"]; - qData.options.forEach((opt, idx) => { - const btn = document.createElement("button"); - btn.className = "choice-btn"; - btn.type = "button"; - btn.innerHTML = `${letters[idx] || "•"} ${opt}`; - btn.onclick = () => submitAnswer(opt, btn); - choiceContainer.appendChild(btn); - }); - } else { - choiceGroup.classList.add("hidden"); - textGroup.classList.remove("hidden"); - input.value = ""; - input.placeholder = t.placeholder; - input.disabled = false; - document.getElementById("checkBtn").disabled = false; - input.focus(); - } - - const hintBtn = document.getElementById("hintBtn"); - if (hintBtn) hintBtn.disabled = false; - const hintBtnMC = document.getElementById("hintBtnMC"); - if (hintBtnMC) hintBtnMC.disabled = false; - - updateProgress(); -} - -function updateProgress() { - const percent = ((currentQuestionNumber - 1) / totalQuestions) * 100; - document.getElementById("progressBar").style.width = percent + "%"; -} - -function checkAnswer() { - const input = document.getElementById("answerInput"); - const answer = input.value.trim(); - if (!answer) { - input.focus(); - return; - } - submitAnswer(answer); -} - -async function submitAnswer(answer, clickedBtn = null) { - const checkBtn = document.getElementById("checkBtn"); - const input = document.getElementById("answerInput"); - const t = UI_TEXT[currentLang] || UI_TEXT.de; - - if (checkBtn) checkBtn.disabled = true; - if (input) input.disabled = true; - - // Disable all choice buttons - const choiceBtns = document.querySelectorAll(".choice-btn"); - choiceBtns.forEach(b => b.disabled = true); - if (clickedBtn) { - clickedBtn.classList.add("choice-selected"); - } - - try { - const res = await fetch("/api/game/check", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - session_id: currentSessionId, - answer: answer - }) - }); - - if (!res.ok) { - throw new Error(currentLang === "en" ? "Answer check failed." : "Fehler bei der Antwortprüfung."); - } - - const result = await res.json(); - currentScore = result.score; - document.getElementById("pointsCounter").innerText = `${t.pointsLabel}: ${currentScore}`; - - const frageBox = document.getElementById("frageBox"); - const feedbackBox = document.getElementById("feedbackBox"); - - if (result.correct) { - // Success! - playSound('magic'); - if (clickedBtn) { - clickedBtn.classList.remove("choice-selected"); - clickedBtn.classList.add("choice-correct"); - } - frageBox.classList.add("sparkle-effect"); - - // Mark question as mastered in history - if (result.question_id) { - recordQuestionMastered(result.question_id); - } - - const pts = result.points_earned || 100; - const ptsText = (currentLang === "en") - ? `✨ Correct! +${pts} House Points! ✨` - : `✨ Richtig! +${pts} Hauspunkte für dein Haus! ✨`; - feedbackBox.innerHTML = `${ptsText}`; - - setTimeout(() => { - frageBox.classList.remove("sparkle-effect"); - if (result.is_finished) { - finishGame(); - } else if (result.next_question) { - currentQuestionNumber = result.next_question.question_number; - attemptsUsed = 0; - renderQuestion(result.next_question); - } - }, 1200); - - } else { - // Incorrect - playSound('wrong'); - if (clickedBtn) { - clickedBtn.classList.remove("choice-selected"); - clickedBtn.classList.add("choice-wrong"); - } - frageBox.classList.add("shake-effect"); - setTimeout(() => frageBox.classList.remove("shake-effect"), 500); - - attemptsUsed = result.attempts_used; - document.getElementById("attemptBadge").innerText = `${t.attemptLabel} ${Math.min(attemptsUsed + 1, maxAttempts)} ${t.ofLabel} ${maxAttempts}`; - - if (result.attempts_left === 0) { - // Exhausted all attempts -> record for spaced repetition learning - if (result.question_id) { - recordQuestionWrong(result.question_id); - } - - feedbackBox.innerHTML = ` -
${t.exhaustedWrong}
-
${t.solutionLabel} ${result.revealed_answer}
- `; - - setTimeout(() => { - if (result.is_finished) { - finishGame(); - } else if (result.next_question) { - currentQuestionNumber = result.next_question.question_number; - attemptsUsed = 0; - renderQuestion(result.next_question); - } - }, 2200); - - } else { - // Still attempts remaining - const msg = t.wrongFeedback.replace("{attempts}", result.attempts_left); - feedbackBox.innerHTML = `${msg}`; - if (checkBtn) checkBtn.disabled = false; - if (input) { - input.disabled = false; - input.select(); - } - // Re-enable choice buttons - choiceBtns.forEach(b => b.disabled = false); - } - } - - } catch (err) { - alert(t.networkError + err.message); - if (checkBtn) checkBtn.disabled = false; - if (input) input.disabled = false; - choiceBtns.forEach(b => b.disabled = false); - } -} - -async function requestHint() { - if (!currentSessionId) return; - const hintBox = document.getElementById("hintBox"); - const hintText = document.getElementById("hintText"); - const t = UI_TEXT[currentLang] || UI_TEXT.de; - - try { - const res = await fetch(`/api/game/hint?session_id=${encodeURIComponent(currentSessionId)}`); - const data = await res.json(); - hintText.innerText = data.hint || t.noHint; - hintBox.classList.remove("hidden"); - } catch (err) { - hintText.innerText = t.hintFailed; - hintBox.classList.remove("hidden"); - } -} - -function finishGame() { - if (timerInterval) clearInterval(timerInterval); - saveRecentQuestions(sessionQuestionIds); - document.getElementById("progressBar").style.width = "100%"; - window.location.href = `/finale?score=${currentScore}&total=${totalQuestions}&time=${elapsedSeconds}&lang=${currentLang}&diff=${encodeURIComponent(selectedDifficulty)}`; -} - -// Enter key to submit -document.addEventListener("DOMContentLoaded", () => { - const input = document.getElementById("answerInput"); - if (input) { - input.addEventListener("keydown", (e) => { - if (e.key === "Enter") { - e.preventDefault(); - checkAnswer(); - } - }); - } - applyQuizTranslations(currentLang); -}); +let currentSessionId = null; +let currentQuestionNumber = 1; +let totalQuestions = 10; +let currentScore = 0; +let currentStreak = 0; +let attemptsUsed = 0; +const maxAttempts = 3; +let selectedDifficulty = "all"; +let selectedCategory = "all"; +let lastQuestionData = null; +let timerStartTime = null; +let timerInterval = null; +let elapsedSeconds = 0; + +const CATEGORY_LABELS = { + de: { + all: "✨ Alle Kategorien", + books: "📖 Buchwissen", + movies: "🎬 Filmwissen", + creatures: "🐉 Magische Geschöpfe", + spells: "🪄 Zaubersprüche & Tränke", + characters: "🧙‍♂️ Charaktere", + hogwarts: "🏰 Hogwarts & Orte", + quidditch: "🧹 Quidditch", + lore: "🔮 Artefakte & Lore", + general: "⚡ Allgemein", + harry_potter: "⚡ Harry Potter" + }, + en: { + all: "✨ All Categories", + books: "📖 Book Lore", + movies: "🎬 Movie Trivia", + creatures: "🐉 Magical Creatures", + spells: "🪄 Spells & Potions", + characters: "🧙‍♂️ Characters", + hogwarts: "🏰 Hogwarts & Places", + quidditch: "🧹 Quidditch", + lore: "🔮 Artifacts & Lore", + general: "⚡ General", + harry_potter: "⚡ Harry Potter" + } +}; + +const UI_TEXT = { + de: { + pageTitle: "Harry Potter Wissensquiz", + pageSubtitle: "Teste dein Wissen über Bücher, Filme, Geschöpfe und Zaubersprüche – direkt & ohne Umwege.", + diffLabel: "Schwierigkeitsgrad", + diffAll: "Alle Stufen", + diffEasy: "Leicht", + diffMed: "Mittel", + diffHard: "Schwer (Experte)", + catLabel: "Themengebiet wählen", + catAll: "Alle Kategorien", + catBooks: "Buchwissen", + catMovies: "Filmwissen", + catCreatures: "Magische Geschöpfe", + catSpells: "Zaubersprüche & Tränke", + catChars: "Charaktere", + catHogwarts: "Hogwarts & Orte", + catQuidditch: "Quidditch", + catLore: "Artefakte & Lore", + dashboardRules: "🎯 10 Fragen pro Runde·🔥 Streak-Zähler·💡 Bis zu 3 Versuche je Frage", + startQuizBtn: "⚡ Quiz jetzt starten ➔", + startBtnCasting: "⚡ Runde wird geladen...", + pointsLabel: "Pkt.", + questionLabel: "Frage", + ofLabel: "/", + attemptLabel: "Versuch", + checkBtn: "✓ Antwort prüfen", + hintBtn: "💡 Tipp anzeigen", + hintHeading: "💡 Hinweis:", + placeholder: "Deine Antwort eingeben...", + noHint: "Kein Tipp vorhanden.", + hintFailed: "Tipp konnte nicht geladen werden.", + wrongFeedback: "Nicht ganz! Noch {attempts} Versuch(e).", + exhaustedWrong: "Leider nicht gelöst (3 Fehlversuche).", + solutionLabel: "Richtige Antwort:", + startError: "Fehler beim Starten des Quiz: ", + networkError: "Netzwerkfehler: " + }, + en: { + pageTitle: "Harry Potter Trivia Quiz", + pageSubtitle: "Test your knowledge across books, movies, creatures, and spells — straight to the action.", + diffLabel: "Difficulty Level", + diffAll: "All Levels", + diffEasy: "Easy", + diffMed: "Medium", + diffHard: "Hard (Expert)", + catLabel: "Select Topic", + catAll: "All Categories", + catBooks: "Book Lore", + catMovies: "Movie Trivia", + catCreatures: "Magical Creatures", + catSpells: "Spells & Potions", + catChars: "Characters", + catHogwarts: "Hogwarts & Places", + catQuidditch: "Quidditch", + catLore: "Artifacts & Lore", + dashboardRules: "🎯 10 Questions per Round·🔥 Answer Streak·💡 Up to 3 Attempts", + startQuizBtn: "⚡ Start Quiz Now ➔", + startBtnCasting: "⚡ Loading Round...", + pointsLabel: "Pts", + questionLabel: "Question", + ofLabel: "/", + attemptLabel: "Attempt", + checkBtn: "✓ Check Answer", + hintBtn: "💡 Show Hint", + hintHeading: "💡 Hint:", + placeholder: "Type your answer...", + noHint: "No hint available.", + hintFailed: "Could not load hint.", + wrongFeedback: "Not quite! {attempts} attempt(s) left.", + exhaustedWrong: "Out of attempts (3 failed tries).", + solutionLabel: "Correct answer:", + startError: "Error starting quiz: ", + networkError: "Network error: " + } +}; + +// Web Audio API Sound Generator for crisp trivia feedback +const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); + +function playSound(type) { + if (audioCtx.state === 'suspended') { + audioCtx.resume(); + } + if (type === 'magic') { + const notes = [523.25, 659.25, 783.99, 1046.50]; // C5, E5, G5, C6 + notes.forEach((freq, index) => { + const osc = audioCtx.createOscillator(); + const gain = audioCtx.createGain(); + osc.type = 'sine'; + osc.frequency.value = freq; + gain.gain.setValueAtTime(0.12, audioCtx.currentTime + index * 0.07); + gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + index * 0.07 + 0.3); + osc.connect(gain); + gain.connect(audioCtx.destination); + osc.start(audioCtx.currentTime + index * 0.07); + osc.stop(audioCtx.currentTime + index * 0.07 + 0.3); + }); + } else if (type === 'wrong') { + const osc = audioCtx.createOscillator(); + const gain = audioCtx.createGain(); + osc.type = 'triangle'; + osc.frequency.setValueAtTime(180, audioCtx.currentTime); + osc.frequency.exponentialRampToValueAtTime(95, audioCtx.currentTime + 0.22); + gain.gain.setValueAtTime(0.12, audioCtx.currentTime); + gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.22); + osc.connect(gain); + gain.connect(audioCtx.destination); + osc.start(); + osc.stop(audioCtx.currentTime + 0.22); + } +} + +function selectDifficulty(diff) { + selectedDifficulty = diff; + document.querySelectorAll(".diff-chip").forEach(chip => { + chip.classList.toggle("active", chip.getAttribute("data-diff") === diff); + }); +} + +function selectCategory(cat) { + selectedCategory = cat; + document.querySelectorAll(".cat-chip").forEach(chip => { + chip.classList.toggle("active", chip.getAttribute("data-cat") === cat); + }); +} + +function updateStreakUI(animate = false) { + const badge = document.getElementById("streakBadge"); + if (!badge) return; + badge.innerText = currentStreak >= 2 ? `🔥 ${currentStreak} Streak` : `🔥 ${currentStreak}`; + badge.classList.toggle("streak-hot", currentStreak >= 2); + if (animate) { + badge.classList.remove("streak-pop"); + void badge.offsetWidth; + badge.classList.add("streak-pop"); + } +} + +function onLanguageChange(lang) { + applyQuizTranslations(lang); +} + +function applyQuizTranslations(lang) { + const t = UI_TEXT[lang] || UI_TEXT.de; + const setTxt = (id, val) => { + const el = document.getElementById(id); + if (el && val) el.innerText = val; + }; + + setTxt("pageTitle", t.pageTitle); + setTxt("pageSubtitle", t.pageSubtitle); + setTxt("diffLabel", t.diffLabel); + setTxt("diffAllText", t.diffAll); + setTxt("diffEasyText", t.diffEasy); + setTxt("diffMedText", t.diffMed); + setTxt("diffHardText", t.diffHard); + setTxt("catLabel", t.catLabel); + setTxt("catAllText", t.catAll); + setTxt("catBooksText", t.catBooks); + setTxt("catMoviesText", t.catMovies); + setTxt("catCreaturesText", t.catCreatures); + setTxt("catSpellsText", t.catSpells); + setTxt("catCharsText", t.catChars); + setTxt("catHogwartsText", t.catHogwarts); + setTxt("catQuidditchText", t.catQuidditch); + setTxt("catLoreText", t.catLore); + + const rulesEl = document.getElementById("dashboardRules"); + if (rulesEl) rulesEl.innerHTML = t.dashboardRules; + + const startQuizBtn = document.getElementById("startQuizBtn"); + if (startQuizBtn && !startQuizBtn.disabled) startQuizBtn.innerText = t.startQuizBtn; + + setTxt("checkBtn", t.checkBtn); + setTxt("hintBtn", t.hintBtn); + setTxt("hintBtnMC", t.hintBtn); + setTxt("hintHeading", t.hintHeading); + + const answerInput = document.getElementById("answerInput"); + if (answerInput) answerInput.placeholder = t.placeholder; + + if (lastQuestionData) { + renderQuestion(lastQuestionData); + } +} + +let sessionQuestionIds = []; + +function getRecentQuestionIds() { + try { + const stored = localStorage.getItem("quiz_recent_qids"); + return stored ? JSON.parse(stored) : []; + } catch(e) { + return []; + } +} + +function getWrongQuestionIds() { + try { + const stored = localStorage.getItem("quiz_wrong_qids"); + return stored ? JSON.parse(stored) : []; + } catch(e) { + return []; + } +} + +function recordQuestionWrong(qid) { + if (!qid) return; + try { + const wrong = getWrongQuestionIds(); + if (!wrong.includes(qid)) { + wrong.push(qid); + if (wrong.length > 30) wrong.shift(); + localStorage.setItem("quiz_wrong_qids", JSON.stringify(wrong)); + } + } catch(e) {} +} + +function recordQuestionMastered(qid) { + if (!qid) return; + try { + let wrong = getWrongQuestionIds(); + if (wrong.includes(qid)) { + wrong = wrong.filter(id => id !== qid); + localStorage.setItem("quiz_wrong_qids", JSON.stringify(wrong)); + } + } catch(e) {} +} + +function saveRecentQuestions(ids) { + if (!ids || ids.length === 0) return; + try { + let recent = getRecentQuestionIds(); + recent = Array.from(new Set([...ids, ...recent])).slice(0, 20); + localStorage.setItem("quiz_recent_qids", JSON.stringify(recent)); + } catch(e) {} +} + +async function startGame() { + const startQuizBtn = document.getElementById("startQuizBtn"); + const t = UI_TEXT[currentLang] || UI_TEXT.de; + + if (startQuizBtn) { + startQuizBtn.disabled = true; + startQuizBtn.innerText = t.startBtnCasting; + } + + playSound('magic'); + sessionQuestionIds = []; + currentStreak = 0; + updateStreakUI(false); + + try { + const recentIds = getRecentQuestionIds(); + const wrongIds = getWrongQuestionIds(); + + const res = await fetch("/api/game/start", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + lang: currentLang, + difficulty: selectedDifficulty, + category: selectedCategory, + recent_ids: recentIds, + wrong_ids: wrongIds + }) + }); + + if (!res.ok) { + throw new Error(currentLang === "en" ? "Could not start game session." : "Konnte Spiel nicht starten."); + } + + const data = await res.json(); + currentSessionId = data.session_id; + currentQuestionNumber = data.question_number; + totalQuestions = data.total_questions; + currentScore = data.score; + attemptsUsed = data.attempts_used; + + const introCard = document.getElementById("introCard"); + if (introCard) { + introCard.classList.add("hidden"); + } + document.getElementById("gameCard").classList.remove("hidden"); + startTimer(); + renderQuestion(data); + + } catch (err) { + alert(t.startError + err.message); + if (startQuizBtn) { + startQuizBtn.disabled = false; + startQuizBtn.innerText = t.startQuizBtn; + } + } +} + +function startTimer() { + timerStartTime = Date.now(); + elapsedSeconds = 0; + if (timerInterval) clearInterval(timerInterval); + timerInterval = setInterval(updateTimer, 1000); + updateTimer(); +} + +function updateTimer() { + if (!timerStartTime) return; + elapsedSeconds = Math.floor((Date.now() - timerStartTime) / 1000); + const m = Math.floor(elapsedSeconds / 60); + const s = elapsedSeconds % 60; + const badge = document.getElementById("timerBadge"); + if (badge) { + badge.innerText = `⏱️ ${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`; + } +} + +function renderQuestion(qData) { + lastQuestionData = qData; + if (qData.id && !sessionQuestionIds.includes(qData.id)) { + sessionQuestionIds.push(qData.id); + } + const t = UI_TEXT[currentLang] || UI_TEXT.de; + const catMap = CATEGORY_LABELS[currentLang] || CATEGORY_LABELS.de; + + document.getElementById("questionCounter").innerText = `${t.questionLabel} ${qData.question_number} ${t.ofLabel} ${totalQuestions}`; + document.getElementById("pointsCounter").innerText = `🏆 ${currentScore} ${t.pointsLabel}`; + + const rawCat = (qData.category || "general").toLowerCase(); + document.getElementById("badgeCategory").innerText = catMap[rawCat] || `⚡ ${rawCat.toUpperCase()}`; + + 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 = currentLang === "en" ? "Hard" : "Schwer"; + diffIcon = "🔴"; + } + badgeDiff.innerText = `${diffIcon} ${diffText}`; + } + + document.getElementById("attemptBadge").innerText = `${t.attemptLabel} ${attemptsUsed + 1} ${t.ofLabel} ${maxAttempts}`; + document.getElementById("frageText").innerText = qData.question; + + const textGroup = document.getElementById("textAnswerGroup"); + const choiceGroup = document.getElementById("choiceAnswerGroup"); + const choiceContainer = document.getElementById("choiceOptionsContainer"); + const input = document.getElementById("answerInput"); + + document.getElementById("hintBox").classList.add("hidden"); + document.getElementById("feedbackBox").innerHTML = ""; + + if (qData.question_type === "multiple_choice" && qData.options && qData.options.length > 0) { + textGroup.classList.add("hidden"); + choiceGroup.classList.remove("hidden"); + choiceContainer.innerHTML = ""; + + const letters = ["A", "B", "C", "D", "E", "F"]; + qData.options.forEach((opt, idx) => { + const btn = document.createElement("button"); + btn.className = "choice-btn"; + btn.type = "button"; + btn.innerHTML = `${letters[idx] || "•"} ${opt}`; + btn.onclick = () => submitAnswer(opt, btn); + choiceContainer.appendChild(btn); + }); + } else { + choiceGroup.classList.add("hidden"); + textGroup.classList.remove("hidden"); + input.value = ""; + input.placeholder = t.placeholder; + input.disabled = false; + document.getElementById("checkBtn").disabled = false; + input.focus(); + } + + const hintBtn = document.getElementById("hintBtn"); + if (hintBtn) hintBtn.disabled = false; + const hintBtnMC = document.getElementById("hintBtnMC"); + if (hintBtnMC) hintBtnMC.disabled = false; + + updateProgress(); +} + +function updateProgress() { + const percent = ((currentQuestionNumber - 1) / totalQuestions) * 100; + document.getElementById("progressBar").style.width = percent + "%"; +} + +function checkAnswer() { + const input = document.getElementById("answerInput"); + const answer = input.value.trim(); + if (!answer) { + input.focus(); + return; + } + submitAnswer(answer); +} + +async function submitAnswer(answer, clickedBtn = null) { + const checkBtn = document.getElementById("checkBtn"); + const input = document.getElementById("answerInput"); + const t = UI_TEXT[currentLang] || UI_TEXT.de; + + if (checkBtn) checkBtn.disabled = true; + if (input) input.disabled = true; + + const choiceBtns = document.querySelectorAll(".choice-btn"); + choiceBtns.forEach(b => b.disabled = true); + if (clickedBtn) { + clickedBtn.classList.add("choice-selected"); + } + + try { + const res = await fetch("/api/game/check", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + session_id: currentSessionId, + answer: answer + }) + }); + + if (!res.ok) { + throw new Error(currentLang === "en" ? "Answer check failed." : "Fehler bei der Antwortprüfung."); + } + + const result = await res.json(); + currentScore = result.score; + document.getElementById("pointsCounter").innerText = `🏆 ${currentScore} ${t.pointsLabel}`; + + const frageBox = document.getElementById("frageBox"); + const feedbackBox = document.getElementById("feedbackBox"); + + if (result.correct) { + playSound('magic'); + currentStreak += 1; + updateStreakUI(true); + + if (clickedBtn) { + clickedBtn.classList.remove("choice-selected"); + clickedBtn.classList.add("choice-correct"); + } + frageBox.classList.add("sparkle-effect"); + + if (result.question_id) { + recordQuestionMastered(result.question_id); + } + + const pts = result.points_earned || 100; + const streakBonusText = currentStreak >= 3 ? ` · 🔥 ${currentStreak}er Serie!` : ""; + const ptsText = (currentLang === "en") + ? `✓ Correct! +${pts} Points${currentStreak >= 3 ? ` · 🔥 ${currentStreak} Streak!` : ""}` + : `✓ Richtig! +${pts} Punkte${streakBonusText}`; + feedbackBox.innerHTML = `${ptsText}`; + + setTimeout(() => { + frageBox.classList.remove("sparkle-effect"); + if (result.is_finished) { + finishGame(); + } else if (result.next_question) { + currentQuestionNumber = result.next_question.question_number; + attemptsUsed = 0; + renderQuestion(result.next_question); + } + }, 1100); + + } else { + playSound('wrong'); + currentStreak = 0; + updateStreakUI(false); + + if (clickedBtn) { + clickedBtn.classList.remove("choice-selected"); + clickedBtn.classList.add("choice-wrong"); + } + frageBox.classList.add("shake-effect"); + setTimeout(() => frageBox.classList.remove("shake-effect"), 500); + + attemptsUsed = result.attempts_used; + document.getElementById("attemptBadge").innerText = `${t.attemptLabel} ${Math.min(attemptsUsed + 1, maxAttempts)} ${t.ofLabel} ${maxAttempts}`; + + if (result.attempts_left === 0) { + if (result.question_id) { + recordQuestionWrong(result.question_id); + } + + feedbackBox.innerHTML = ` +
${t.exhaustedWrong}
+
${t.solutionLabel} ${result.revealed_answer}
+ `; + + setTimeout(() => { + if (result.is_finished) { + finishGame(); + } else if (result.next_question) { + currentQuestionNumber = result.next_question.question_number; + attemptsUsed = 0; + renderQuestion(result.next_question); + } + }, 2200); + + } else { + const msg = t.wrongFeedback.replace("{attempts}", result.attempts_left); + feedbackBox.innerHTML = `${msg}`; + if (checkBtn) checkBtn.disabled = false; + if (input) { + input.disabled = false; + input.select(); + } + choiceBtns.forEach(b => b.disabled = false); + } + } + + } catch (err) { + alert(t.networkError + err.message); + if (checkBtn) checkBtn.disabled = false; + if (input) input.disabled = false; + choiceBtns.forEach(b => b.disabled = false); + } +} + +async function requestHint() { + if (!currentSessionId) return; + const hintBox = document.getElementById("hintBox"); + const hintText = document.getElementById("hintText"); + const t = UI_TEXT[currentLang] || UI_TEXT.de; + + try { + const res = await fetch(`/api/game/hint?session_id=${encodeURIComponent(currentSessionId)}`); + const data = await res.json(); + hintText.innerText = data.hint || t.noHint; + hintBox.classList.remove("hidden"); + } catch (err) { + hintText.innerText = t.hintFailed; + hintBox.classList.remove("hidden"); + } +} + +function finishGame() { + if (timerInterval) clearInterval(timerInterval); + saveRecentQuestions(sessionQuestionIds); + document.getElementById("progressBar").style.width = "100%"; + window.location.href = `/finale?score=${currentScore}&total=${totalQuestions}&time=${elapsedSeconds}&lang=${currentLang}&diff=${encodeURIComponent(selectedDifficulty)}`; +} + +document.addEventListener("DOMContentLoaded", () => { + const input = document.getElementById("answerInput"); + if (input) { + input.addEventListener("keydown", (e) => { + if (e.key === "Enter") { + e.preventDefault(); + checkAnswer(); + } + }); + } + applyQuizTranslations(currentLang); +}); diff --git a/app/static/js/vorschlag.js b/app/static/js/vorschlag.js index b689332..584751c 100755 --- a/app/static/js/vorschlag.js +++ b/app/static/js/vorschlag.js @@ -1,18 +1,20 @@ const SUGGESTION_TEXT = { de: { - title: "✨ Neue Frage für das Quiz vorschlagen ✨", - subtitle: "Bereichere das magische Quiz mit kniffligen Harry-Potter- oder Escape-Rätseln!", - alert: "ℹ️ Hinweis zur Moderation: Eingereichte Fragen werden nicht sofort öffentlich sichtbar, sondern erhalten automatisch den Status „Prüfung ausstehend“. Unser Zaubereiministerium prüft und testet die Antwort, bevor die Frage freigeschaltet wird.", - nameLabel: "Dein Name / Zauberer-Alias (optional):", + title: "✍️ Neue Quiz-Frage einreichen", + subtitle: "Bereichere PotterTrivia mit deinen eigenen Fragen zu Büchern, Filmen oder magischen Geschöpfen!", + alert: "ℹ️ Hinweis zur Moderation: Eingereichte Fragen werden kurz auf Richtigkeit geprüft, bevor sie im öffentlichen Quiz-Pool freigeschaltet werden.", + nameLabel: "Dein Name / Alias (optional):", namePlaceholder: "z. B. Hermine Granger", catLabel: "Kategorie / Themenbereich:", - catHogwarts: "🏰 Hogwarts & Schulgelände", - catCharacters: "🧙 Charaktere & Personen", - catSpells: "⚡ Zaubersprüche & Magie", - catCreatures: "🐉 Magische Tierwesen", + catBooks: "📖 Buchwissen (Buch-Details)", + catMovies: "🎬 Filmwissen (Kino & Drehorte)", + catHogwarts: "🏰 Hogwarts & Orte", + catCharacters: "🧙‍♂️ Charaktere & Personen", + catSpells: "🪄 Zaubersprüche & Tränke", + catCreatures: "🐉 Magische Geschöpfe", catQuidditch: "🧹 Quidditch & Sport", catLore: "🔮 Artefakte & Geschichte", - catGeneral: "📜 Allgemeines Zauberwissen", + catGeneral: "⚡ Allgemeines Wissen", frageLabel: "Deine Frage: *", fragePlaceholder: "z. B. Wie heißt der Schulleiter von Hogwarts?", diffLabel: "Schwierigkeitsgrad:", @@ -36,19 +38,21 @@ const SUGGESTION_TEXT = { successHeader: "✨ Vorschlag erfolgreich übermittelt! ✨" }, en: { - title: "✨ Suggest a New Quiz Question ✨", - subtitle: "Enrich the magical quiz with tricky Harry Potter or escape puzzles!", - alert: "ℹ️ Moderation Note: Submitted questions are not immediately public; they automatically receive the status 'Pending Review'. The Ministry of Magic tests and verifies the answer before approval.", - nameLabel: "Your Name / Wizard Alias (optional):", + title: "✍️ Submit a New Quiz Question", + subtitle: "Enrich PotterTrivia with your own questions about the books, movies, or magical creatures!", + alert: "ℹ️ Moderation Note: Submitted questions are briefly reviewed for accuracy before going live in the public quiz pool.", + nameLabel: "Your Name / Alias (optional):", namePlaceholder: "e.g. Hermione Granger", catLabel: "Category / Topic Area:", - catHogwarts: "🏰 Hogwarts & School Grounds", - catCharacters: "🧙 Characters & People", - catSpells: "⚡ Spells & Magic", + catBooks: "📖 Book Lore (Book Details)", + catMovies: "🎬 Movie Trivia (Cinema & Filming)", + catHogwarts: "🏰 Hogwarts & Places", + catCharacters: "🧙‍♂️ Characters & People", + catSpells: "🪄 Spells & Potions", catCreatures: "🐉 Magical Creatures", catQuidditch: "🧹 Quidditch & Sport", catLore: "🔮 Artifacts & Lore", - catGeneral: "📜 General Wizarding Lore", + catGeneral: "⚡ General Knowledge", frageLabel: "Your Question: *", fragePlaceholder: "e.g. Who is the Headmaster of Hogwarts?", diffLabel: "Difficulty Level:", @@ -92,6 +96,8 @@ function onLanguageChange(lang) { const submitBtn = document.getElementById("submitBtn"); const catLabel = document.getElementById("suggCatLabel"); + const catBooks = document.getElementById("catBooks"); + const catMovies = document.getElementById("catMovies"); const catHogwarts = document.getElementById("catHogwarts"); const catCharacters = document.getElementById("catCharacters"); const catSpells = document.getElementById("catSpells"); @@ -106,6 +112,8 @@ function onLanguageChange(lang) { if (nameLabel) nameLabel.innerText = t.nameLabel; if (nameInput) nameInput.placeholder = t.namePlaceholder; if (catLabel) catLabel.innerText = t.catLabel; + if (catBooks) catBooks.innerText = t.catBooks; + if (catMovies) catMovies.innerText = t.catMovies; if (catHogwarts) catHogwarts.innerText = t.catHogwarts; if (catCharacters) catCharacters.innerText = t.catCharacters; if (catSpells) catSpells.innerText = t.catSpells; diff --git a/app/templates/admin.html b/app/templates/admin.html index 5b2ec1f..651c733 100755 --- a/app/templates/admin.html +++ b/app/templates/admin.html @@ -225,9 +225,15 @@
diff --git a/app/templates/base.html b/app/templates/base.html index 7c67af5..6a76ae6 100755 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -1,167 +1,117 @@ - - - - - - {% block title %}Magisches Harry-Potter-Escape-Quiz{% endblock %} - - - - - - {% block head %}{% endblock %} - - - - - - -
- {% block content %}{% endblock %} -
- - - - - {% block scripts %}{% endblock %} - - + + + + + + {% block title %}PotterTrivia – Das Harry-Potter-Wissensquiz{% endblock %} + + + + + + {% block head %}{% endblock %} + + + + +
+ {% block content %}{% endblock %} +
+ + + + + {% block scripts %}{% endblock %} + + diff --git a/app/templates/index.html b/app/templates/index.html index 761ec6e..db35459 100755 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -1,174 +1,160 @@ -{% extends "base.html" %} - -{% block title %}✨ Magisches Harry-Potter-Escape-Quiz ✨{% endblock %} - -{% block content %} -
-
-

✨ Magisches Hogwarts-Escape-Quiz ✨

-

10 knifflige Rätsel aus der Zauberwelt warten auf dich

-
- - -
- -
-
- -
- - -
-
-
-
- Hogwarts Crest -
-

Hogwarts-Schule für Hexerei und Zauberei

-

Schulleiter: Albus Dumbledore
(Großhexenmeister, Träger des Merlin-Ordens 1. Klasse)

-
- ❦ - - ✦ - - ❧ -
-
- -
-

Liebe Hexe, lieber Zauberer,

-

- wir freuen uns, dir mitteilen zu dürfen, dass du zu den Magischen Z.A.G.-Prüfungen an der Hogwarts-Schule zugelassen bist. -

-

- Zehn anspruchsvolle Prüfungsfragen entscheiden über deinen Rang in den Chroniken. Zeige deinen Scharfsinn und sichere bis zu 1.000 Hauspunkte für dein Haus! -

-
- -
-

Mit vorzüglicher Hochachtung,

-

Minerva McGonagall

-

Stellvertretende Schulleiterin

-
-
- - -
-
-
-

Hogwarts-Schule für Hexerei und Zauberei

-

An den ehrenwerten Zauberschüler

-

Auf dem Prüfungspfad • Schloss Hogwarts

-
-
- - -
-
-
- - -
- Hogwarts-Wappen Siegel -
-
-
-
-
- - -
-

- Ein verzauberter Pergamentbrief aus Hogwarts liegt vor dir.
- Wähle deinen Schwierigkeitsgrad und brich das rote Zaubersiegel: -

- - -
- Schwierigkeitsgrad wählen: -
- - - - -
-
- - -
-
- - - -
-{% endblock %} - -{% block scripts %} - -{% endblock %} +{% extends "base.html" %} + +{% block title %}⚡ PotterTrivia – Das Harry-Potter-Wissensquiz{% endblock %} + +{% block content %} +
+
+
⚡ {{ question_count }} Fragen im Wissens-Pool · Buch & Film
+

Harry Potter Wissensquiz

+

Teste dein Wissen über Bücher, Filme, Geschöpfe und Zaubersprüche – direkt & ohne Umwege.

+
+ + +
+
+ +
+
+ 1 + Schwierigkeitsgrad +
+
+ + + + +
+
+ + +
+
+ 2 + Themengebiet wählen +
+
+ + + + + + + + + +
+
+
+ + +
+ + + +
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/app/templates/vorschlag.html b/app/templates/vorschlag.html index f8d854e..d1acc1e 100755 --- a/app/templates/vorschlag.html +++ b/app/templates/vorschlag.html @@ -5,11 +5,11 @@ {% block content %}
-

✨ Neue Frage für das Quiz vorschlagen ✨

-

Bereichere das magische Quiz mit kniffligen Harry-Potter- oder Escape-Rätseln!

+

✍️ Neue Quiz-Frage einreichen

+

Bereichere PotterTrivia mit deinen eigenen Fragen zu Büchern, Filmen oder magischen Geschöpfen!

- ℹ️ Hinweis zur Moderation: Eingereichte Fragen werden nicht sofort öffentlich sichtbar, sondern erhalten automatisch den Status „Prüfung ausstehend“. Unser Zaubereiministerium prüft und testet die Antwort, bevor die Frage freigeschaltet wird. + ℹ️ Hinweis zur Moderation: Eingereichte Fragen werden kurz auf Richtigkeit geprüft, bevor sie im öffentlichen Quiz-Pool freigeschaltet werden.
@@ -26,13 +26,15 @@
diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a91d739 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,29 @@ +annotated-doc==0.0.5 +annotated-types==0.8.0 +anyio==4.15.1 +cffi==2.1.1 +click==8.5.0 +cryptography==50.0.1 +fastapi==0.141.1 +greenlet==3.5.6 +h11==0.16.0 +httptools==0.8.0 +idna==3.20 +itsdangerous==2.2.0 +Jinja2==3.1.6 +MarkupSafe==3.0.3 +pycparser==3.0 +pydantic==2.13.5 +pydantic_core==2.46.5 +PyMySQL==1.2.3 +python-dotenv==1.2.3 +python-multipart==0.0.32 +PyYAML==6.0.3 +SQLAlchemy==2.0.54 +starlette==1.6.0 +typing-inspection==0.4.4 +typing_extensions==4.16.0 +uvicorn==0.53.0 +uvloop==0.22.1 +watchfiles==1.3.0 +websockets==17.1