diff --git a/app/main.py b/app/main.py index 38c8d49..c0a672c 100755 --- a/app/main.py +++ b/app/main.py @@ -12,11 +12,11 @@ from sqlalchemy.orm import Session from dotenv import load_dotenv from app.database import get_db -from app.models import Question, QuizSession, AdminUser +from app.models import Question, QuizSession, AdminUser, LeaderboardEntry from app.matcher import is_answer_correct, levenshtein_distance, normalize_text from app.auth import verify_password from app.seed import seed_db -from app.schemas import GameCheckRequest, SuggestionCreateRequest, GameStartRequest +from app.schemas import GameCheckRequest, SuggestionCreateRequest, GameStartRequest, LeaderboardSubmitRequest load_dotenv() @@ -58,6 +58,81 @@ def format_question_for_client(q: Question, number: int, total: int, lang: str = "difficulty": q.difficulty or "medium" } +def get_tier_evaluation(score: int, total: int = 10, lang: str = "de") -> dict: + pct = (score / total) if total > 0 else 0 + is_de = (lang != "en") + + if score >= 10 or pct >= 1.0: + grade = "O" + title = "🧙♂️ Großmeister der Zauberei" if is_de else "🧙♂️ Grandmaster of Wizardry" + subtitle = "Note: O (Ohnegleichen)" if is_de else "Grade: O (Outstanding)" + desc = ( + "Unglaublich! Albus Dumbledore überlegt bereits, dir das Büro des Schulleiters zu überlassen. " + "Du kennst selbst die geheimsten Winkel von Hogwarts besser als der Fast Kopflose Nick. " + "Das Zaubereiministerium verneigt sich in Ehrfurcht!" + if is_de else + "Incredible! Albus Dumbledore is already considering handing you the headmaster's office. " + "You know the secret passages of Hogwarts better than Nearly Headless Nick. " + "The Ministry of Magic bows in deep awe!" + ) + badge_class = "tier-outstanding" + elif score >= 8 or pct >= 0.8: + grade = "E" + title = "⚡ Meister-Hexe / Zauberer" if is_de else "⚡ Master Witch / Wizard" + subtitle = "Note: E (Erwartungen übertroffen)" if is_de else "Grade: E (Exceeds Expectations)" + desc = ( + "Hermine Granger nickt anerkennend und Professor McGonagall vergibt 50 Punkte für dein Haus! " + "Dein Zauberstab zuckt blitzschnell und dein Wissen lässt selbst Ravenclaws erblassen." + if is_de else + "Hermione Granger nods in deep approval and Professor McGonagall awards 50 points to your House! " + "Your wandwork is razor-sharp and your knowledge makes even Ravenclaws jealous." + ) + badge_class = "tier-exceeds" + elif score >= 5 or pct >= 0.5: + grade = "A" + title = "📜 Solider Hogwarts-Schüler" if is_de else "📜 Solid Hogwarts Student" + subtitle = "Note: A (Annehmbar)" if is_de else "Grade: A (Acceptable)" + desc = ( + "Bestanden! Ein paar Kessel sind zwar im Zaubertränke-Unterricht explodiert und Peeves hat dich mit Kreide beworfen, " + "aber du hast dich tapfer durchgeschlagen. Eine solide magische Leistung!" + if is_de else + "Passed! A few cauldrons blew up during Potions class and Peeves threw chalk at your head, " + "but you bravely fought your way through. A solid magical performance!" + ) + badge_class = "tier-acceptable" + elif score >= 2 or pct >= 0.2: + grade = "M" + title = "🧹 Quaffel-Fänger in Not" if is_de else "🧹 Quaffle Catcher in Distress" + subtitle = "Note: M (Mies)" if is_de else "Grade: P (Poor)" + desc = ( + "Au weia... Hat Ron Weasley dir seinen mit Tesafilm geklebten Zauberstab geliehen? " + "Deine Eule hat sich wohl im Nebel verflogen. Zeit für ein paar Extrastunden in der Bibliothek bei Madam Pince!" + if is_de else + "Yikes... Did Ron Weasley lend you his Spellotaped wand? " + "Your owl must have gotten lost in the fog. Time for extra study sessions in the library with Madam Pince!" + ) + badge_class = "tier-poor" + else: + grade = "T" + title = "🧌 Bergtroll im Kerker" if is_de else "🧌 Mountain Troll in the Dungeon" + subtitle = "Note: T (Troll)" if is_de else "Grade: T (Troll)" + desc = ( + "Alarm im Schloss! Du hast mit der Holzkeule auf die Fragen eingeschlagen. " + "Schnell weg, bevor Hausmeister Filch dich beim lebenslangen Nachsitzen in den Kerkern erwischt!" + if is_de else + "Troll in the dungeon! You smashed the questions with a wooden club. " + "Run quickly before caretaker Filch catches you for lifelong detention in the dungeons!" + ) + badge_class = "tier-troll" + + return { + "grade": grade, + "title": title, + "subtitle": subtitle, + "description": desc, + "badge_class": badge_class + } + @app.on_event("startup") def startup_event(): seed_db() @@ -78,19 +153,16 @@ async def start_game(payload: GameStartRequest = None, db: Session = Depends(get 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" - # Fetch active questions matching difficulty (or all active) query = db.query(Question).filter(Question.status == "active") if difficulty != "all": query = query.filter(Question.difficulty == difficulty) pool = query.all() if not pool: - # Fallback to all active if filtered pool empty pool = db.query(Question).filter(Question.status == "active").all() if not pool: raise HTTPException(status_code=400, detail="Keine aktiven Fragen vorhanden.") - # Randomly sample 10 questions (or all if fewer) sample_size = min(10, len(pool)) selected_questions = random.sample(pool, sample_size) question_ids = [q.id for q in selected_questions] @@ -104,7 +176,8 @@ async def start_game(payload: GameStartRequest = None, db: Session = Depends(get attempts_current=0, is_finished=False, lang=lang, - difficulty=difficulty + difficulty=difficulty, + created_at=datetime.datetime.utcnow() ) db.add(new_session) db.commit() @@ -131,6 +204,9 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db)) question_ids = json.loads(session.question_ids) if session.current_question_idx >= len(question_ids): session.is_finished = True + session.finished_at = datetime.datetime.utcnow() + if session.created_at: + session.duration_seconds = max(1, int((session.finished_at - session.created_at).total_seconds())) db.commit() return { "correct": False, @@ -138,6 +214,7 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db)) "attempts_used": session.attempts_current, "attempts_left": 0, "is_finished": True, + "duration_seconds": session.duration_seconds, "next_question": None } @@ -157,6 +234,8 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db)) if is_finished: session.is_finished = True session.finished_at = datetime.datetime.utcnow() + if session.created_at: + session.duration_seconds = max(1, int((session.finished_at - session.created_at).total_seconds())) db.commit() @@ -172,6 +251,7 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db)) "attempts_used": 0, "attempts_left": 3, "is_finished": is_finished, + "duration_seconds": session.duration_seconds, "revealed_answer": None, "next_question": next_q_data } @@ -180,7 +260,6 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db)) attempts_left = max(0, 3 - session.attempts_current) if attempts_left == 0: - # 3 failed attempts: move to next question, reveal answer if session_lang == "en" and current_q.answer_en: revealed = current_q.answer_en else: @@ -193,6 +272,8 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db)) if is_finished: session.is_finished = True session.finished_at = datetime.datetime.utcnow() + if session.created_at: + session.duration_seconds = max(1, int((session.finished_at - session.created_at).total_seconds())) db.commit() @@ -208,6 +289,7 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db)) "attempts_used": 3, "attempts_left": 0, "is_finished": is_finished, + "duration_seconds": session.duration_seconds, "revealed_answer": revealed, "next_question": next_q_data } @@ -247,13 +329,125 @@ async def get_hint(session_id: str, db: Session = Depends(get_db)): return {"hint": hint_text} @app.get("/finale", response_class=HTMLResponse) -async def finale(request: Request, score: int = 0, total: int = 10, lang: str = "de"): +async def finale(request: Request, score: int = 0, total: int = 10, time: int = 0, lang: str = "de"): + tier = get_tier_evaluation(score, total, lang) + if time >= 60: + minutes = time // 60 + secs = time % 60 + time_str = f"{minutes} Min. {secs} Sek." if lang != "en" else f"{minutes} min. {secs} sec." + else: + time_str = f"{time} Sek." if lang != "en" else f"{time} sec." + return templates.TemplateResponse( request=request, name="finale.html", - context={"score": score, "total": total, "lang": lang} + context={ + "score": score, + "total": total, + "time_seconds": time, + "time_formatted": time_str, + "lang": lang, + "tier": tier + } ) +@app.get("/bestenliste", response_class=HTMLResponse) +async def bestenliste_page(request: Request, lang: str = "de"): + return templates.TemplateResponse( + request=request, + name="bestenliste.html", + context={"lang": lang} + ) + +@app.get("/impressum", response_class=HTMLResponse) +async def impressum_page(request: Request, lang: str = "de"): + return templates.TemplateResponse( + request=request, + name="impressum.html", + context={"lang": lang} + ) + +@app.get("/datenschutz", response_class=HTMLResponse) +async def datenschutz_page(request: Request, lang: str = "de"): + return templates.TemplateResponse( + request=request, + name="datenschutz.html", + context={"lang": lang} + ) + +@app.post("/api/leaderboard") +async def submit_leaderboard(payload: LeaderboardSubmitRequest, db: Session = Depends(get_db)): + nick = payload.nickname.strip() + if not nick: + wizard_adjectives = ["Mutiger", "Schlauer", "Weiser", "Flinke", "Listiger", "Fliegender"] + wizard_nouns = ["Greif", "Phönix", "Zauberer", "Drache", "Niffler", "Thestral"] + nick = f"{random.choice(wizard_adjectives)}_{random.choice(wizard_nouns)}_{random.randint(10, 99)}" + nick = nick[:30] + + time_sec = max(1, payload.time_seconds) + diff = payload.difficulty if payload.difficulty in ["easy", "medium", "hard"] else "all" + + entry = LeaderboardEntry( + nickname=nick, + score=payload.score, + total_questions=payload.total_questions or 10, + time_seconds=time_sec, + difficulty=diff, + lang=payload.lang or "de", + session_id=payload.session_id, + created_at=datetime.datetime.utcnow() + ) + db.add(entry) + db.commit() + db.refresh(entry) + + better_count = db.query(LeaderboardEntry).filter( + (LeaderboardEntry.score > entry.score) | + ((LeaderboardEntry.score == entry.score) & (LeaderboardEntry.time_seconds < entry.time_seconds)) | + ((LeaderboardEntry.score == entry.score) & (LeaderboardEntry.time_seconds == entry.time_seconds) & (LeaderboardEntry.id < entry.id)) + ).count() + rank = better_count + 1 + + return { + "success": True, + "entry_id": entry.id, + "rank": rank, + "nickname": entry.nickname, + "score": entry.score, + "total_questions": entry.total_questions, + "time_seconds": entry.time_seconds + } + +@app.get("/api/leaderboard") +async def get_leaderboard(difficulty: str = None, limit: int = 20, db: Session = Depends(get_db)): + query = db.query(LeaderboardEntry) + if difficulty and difficulty != "all": + query = query.filter(LeaderboardEntry.difficulty == difficulty) + + entries = query.order_by( + LeaderboardEntry.score.desc(), + LeaderboardEntry.time_seconds.asc(), + LeaderboardEntry.created_at.asc() + ).limit(min(100, limit)).all() + + result = [] + for idx, e in enumerate(entries, 1): + m = e.time_seconds // 60 + s = e.time_seconds % 60 + time_str = f"{m:02d}:{s:02d}" + result.append({ + "rank": idx, + "id": e.id, + "nickname": e.nickname, + "score": e.score, + "total_questions": e.total_questions, + "time_seconds": e.time_seconds, + "time_formatted": time_str, + "difficulty": e.difficulty, + "date": e.created_at.strftime("%d.%m.%Y") if e.created_at else "" + }) + return result + @app.get("/vorschlag", response_class=HTMLResponse) async def vorschlag_page(request: Request): return templates.TemplateResponse( diff --git a/app/models.py b/app/models.py index 7c6f62e..7568ad6 100755 --- a/app/models.py +++ b/app/models.py @@ -41,9 +41,23 @@ class QuizSession(Base): is_finished = Column(Boolean, default=False) lang = Column(String(10), default="de") # 'de' or 'en' difficulty = Column(String(20), default="all") # 'all', 'easy', 'medium', 'hard' + duration_seconds = Column(Integer, nullable=True) created_at = Column(DateTime, default=datetime.datetime.utcnow) finished_at = Column(DateTime, nullable=True) +class LeaderboardEntry(Base): + __tablename__ = "leaderboard_entries" + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + nickname = Column(String(50), nullable=False, index=True) + score = Column(Integer, nullable=False, index=True) + total_questions = Column(Integer, default=10) + time_seconds = Column(Integer, nullable=False, index=True) + difficulty = Column(String(20), default="all", index=True) + lang = Column(String(10), default="de") + session_id = Column(String(36), nullable=True) + created_at = Column(DateTime, default=datetime.datetime.utcnow, index=True) + class AdminUser(Base): __tablename__ = "admin_users" diff --git a/app/schemas.py b/app/schemas.py index b7ea9e8..6659bf5 100755 --- a/app/schemas.py +++ b/app/schemas.py @@ -40,6 +40,15 @@ class SuggestionCreateRequest(BaseModel): falsche_antworten: Optional[List[str]] = None difficulty: Optional[str] = "medium" +class LeaderboardSubmitRequest(BaseModel): + nickname: str + score: int + total_questions: int = 10 + time_seconds: int + difficulty: Optional[str] = "all" + lang: Optional[str] = "de" + session_id: Optional[str] = None + class QuestionAdminCreate(BaseModel): category: str = "harry_potter" question: str diff --git a/app/static/css/style.css b/app/static/css/style.css index f29ff20..e161d55 100755 --- a/app/static/css/style.css +++ b/app/static/css/style.css @@ -604,6 +604,15 @@ body { 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; @@ -847,6 +856,172 @@ body { 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; +} + +/* 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 ========================================= */ @@ -1069,6 +1244,69 @@ body { margin-top: auto; } +.footer-links { + margin-top: 8px; + display: flex; + justify-content: center; + align-items: center; + gap: 10px; +} + +.footer-link { + color: var(--text-muted); + text-decoration: none; + font-size: 0.8rem; + transition: color 0.2s ease; +} + +.footer-link:hover { + color: var(--gold-bright); + text-decoration: underline; +} + +.footer-sep { + color: rgba(255, 255, 255, 0.2); +} + +.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; } diff --git a/app/static/js/quiz.js b/app/static/js/quiz.js index 6c38a7e..bc2eccd 100755 --- a/app/static/js/quiz.js +++ b/app/static/js/quiz.js @@ -6,6 +6,9 @@ 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: { @@ -24,8 +27,8 @@ const UI_TEXT = { questionLabel: "Frage", ofLabel: "von", attemptLabel: "Versuch", - checkBtn: "✨ Zauber prüfen", - hintBtn: "💡 Zaubertipp", + checkBtn: "✨ Antwort prüfen", + hintBtn: "💡 Hinweis anzeigen", hintHeading: "📜 Notiz aus der verbotenen Abteilung:", placeholder: "Deine Antwort hier eingeben...", noHint: "Kein Tipp vorhanden.", @@ -53,8 +56,8 @@ const UI_TEXT = { questionLabel: "Question", ofLabel: "of", attemptLabel: "Attempt", - checkBtn: "✨ Check Spell", - hintBtn: "💡 Magic Hint", + checkBtn: "✨ Check Answer", + hintBtn: "💡 Show Hint", hintHeading: "📜 Note from the Restricted Section:", placeholder: "Type your answer here...", noHint: "No hint available.", @@ -203,6 +206,7 @@ async function startGame() { setTimeout(() => { document.getElementById("introCard").classList.add("hidden"); document.getElementById("gameCard").classList.remove("hidden"); + startTimer(); renderQuestion(data); }, 1100); @@ -214,6 +218,25 @@ async function startGame() { } } +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; const t = UI_TEXT[currentLang] || UI_TEXT.de; @@ -424,8 +447,9 @@ async function requestHint() { } function finishGame() { + if (timerInterval) clearInterval(timerInterval); document.getElementById("progressBar").style.width = "100%"; - window.location.href = `/finale?score=${currentScore}&total=${totalQuestions}&lang=${currentLang}`; + window.location.href = `/finale?score=${currentScore}&total=${totalQuestions}&time=${elapsedSeconds}&lang=${currentLang}`; } // Enter key to submit diff --git a/app/templates/base.html b/app/templates/base.html index e944bfa..17c1860 100755 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -36,6 +36,7 @@ @@ -48,6 +49,11 @@ +{% endblock %} diff --git a/app/templates/datenschutz.html b/app/templates/datenschutz.html new file mode 100644 index 0000000..87c8cce --- /dev/null +++ b/app/templates/datenschutz.html @@ -0,0 +1,254 @@ +{% extends "base.html" %} + +{% block title %}{% if lang == 'en' %}🛡️ Privacy Policy (Datenschutz){% else %}🛡️ Datenschutzerklärung{% endif %}{% endblock %} + +{% block content %} +
+ {% if lang == 'en' %}Information pursuant to the EU General Data Protection Regulation (GDPR / DSGVO){% else %}Informationen zur Datenverarbeitung gemäß Art. 13 DSGVO{% endif %} +
+ +
+ Responsible for data processing on dev.waiswuaschd.de:
+ Maximilian R. (Waiswuaschd Project)
+ Email: kontakt@waiswuaschd.de
+
+ We treat your data with the utmost care. This portal is built with strict data minimization in mind: + No user accounts are required, no tracking tools (e.g. Google Analytics, Meta Pixel) are used, and no data is sold to third parties. +
+ ++ When accessing our website, the web server automatically records standard access data (server logs): +
++ This data is processed pursuant to Art. 6(1)(f) GDPR to ensure technical system stability and IT security (e.g. DDoS protection). Log files are automatically deleted after standard server retention periods. +
+ ++ We use strictly necessary technical storage only: +
+quiz_session): A temporary identifier required to coordinate your 10 quiz questions. It expires when you close your browser session.quiz_lang) and background music setting (bgm_enabled) between pages.
+ Entering your name into the Hogwarts Hall of Fame (Leaderboard) or submitting a quiz question is completely voluntary.
+ We explicitly ask you not to provide your real name, but rather a playful wizard alias or nickname (e.g. Golden_Snitch_99).
+
+ Under EU GDPR, you have the right to information (Art. 15), correction (Art. 16), deletion (Art. 17), and restriction of processing (Art. 18). + For any data privacy inquiries, please contact us at kontakt@waiswuaschd.de. +
+ {% else %} +
+ Verantwortlich für die Datenverarbeitung auf dev.waiswuaschd.de:
+ Maximilian R. (Waiswuaschd Projekt)
+ E-Mail: kontakt@waiswuaschd.de
+
+ Der Schutz deiner persönlichen Daten ist uns ein wichtiges Anliegen. Unser Quiz-Portal ist nach dem Prinzip der Datensparsamkeit konzipiert: + Es gibt keine Pflicht zur Registrierung, keine Tracking-Tools (wie Google Analytics oder Facebook-Pixel) und keine Weitergabe von Daten an Werbedienstleister. +
+ ++ Beim Aufrufen unserer Webseiten erfasst der Webserver automatisch technische Informationen (Server-Logfiles): +
++ Die Verarbeitung erfolgt auf Grundlage von Art. 6 Abs. 1 lit. f DSGVO zur Gewährleistung der Systemsicherheit, Stabilität und Abwehr von Cyberangriffen. Logfiles werden nach Ablauf der regulären Server-Aufbewahrungsfrist automatisch gelöscht. +
+ ++ Wir setzen ausschließlich technisch notwendige Hilfsmittel ein: +
+quiz_session): Wird benötigt, um deinen Spielstand und deine 10 Fragen während der laufenden Quizrunde zuzuordnen.quiz_lang) und Musikpräferenz (bgm_enabled), damit du diese beim nächsten Seitenaufruf nicht neu einstellen musst.
+ Der Eintrag in die Hogwarts-Ruhmeshalle (Bestenliste) sowie das Einreichen eigener Quizfragen erfolgt auf rein freiwilliger Basis.
+ Wir bitten dich ausdrücklich darum, keine Klarnamen einzugeben, sondern ein magisches Pseudonym bzw. einen Alias (z. B. Zauberlehrling_42).
+
+ Du hast nach der DSGVO jederzeit das Recht auf unentgeltliche Auskunft über deine gespeicherten Daten (Art. 15), Berichtigung (Art. 16) oder Löschung (Art. 17). Wende dich bei Fragen gerne direkt per E-Mail an uns. +
+ {% endif %} +
- {% if lang == 'en' %}
- We'll order delicious food and make ourselves totally comfortable: relax, laugh, and enjoy quality time together.
- Plus, we'll play a brand new Online Escape Game of your choice!
- {% else %}
- Wir holen uns lecker Essen und machen es uns so richtig gemütlich: entspannen, lachen und die gemeinsame Zeit genießen.
- Dazu spielen wir ein brandneues Online Escape Game deiner Wahl!
- {% endif %}
-
- {% if lang == 'en' %} - I'm so excited for our wonderful evening together! 💫 - {% else %} - Ich freue mich riesig auf unseren gemeinsamen Abend! 💫 - {% endif %} -
+ +{{ tier.subtitle }}
+{{ tier.description }}
+Völlig anonym – wähle deinen Zauberer-Spitznamen:
+ + + + + + ++ {% if lang == 'en' %}Information according to § 5 DDG{% else %}Angaben gemäß § 5 Digitale-Dienste-Gesetz (DDG){% endif %} +
+ +
+ Magisches Hogwarts Escape Quiz
+ A private, non-commercial web project on dev.waiswuaschd.de.
+ Operator: Maximilian R. (Waiswuaschd Project)
+ Contact: kontakt@waiswuaschd.de
+
+ This website is a purely non-commercial, private fan project created for educational, puzzle, and entertainment purposes. + No commercial profit or advertising revenue is generated through this service. +
+ ++ Harry Potter characters, names, and related indicia are trademarks of and © Warner Bros. Entertainment Inc. and J.K. Rowling. + Harry Potter publishing rights © Bloomsbury Publishing Plc and Carlsen Verlag. + All rights belong to their respective copyright and trademark owners. No infringement is intended. +
+ ++ As a service provider, we are responsible for our own content on these pages under general laws. We make every effort to keep information up to date and accurate, but cannot accept liability for external links to third-party websites over whose content we have no control. +
+ {% else %} +
+ Magisches Hogwarts-Escape-Quiz
+ Ein rein privates, nicht-kommerzielles Projekt auf dev.waiswuaschd.de.
+ Betreiber: Maximilian R. (Waiswuaschd Projekt)
+ E-Mail-Kontakt: kontakt@waiswuaschd.de
+
+ Diese Webseite ist ein reines Fan-Projekt, das unentgeltlich und aus Freude an Zauberrätseln und Harry Potter betrieben wird. Es werden keinerlei kommerzielle Interessen verfolgt, keine Werbung geschaltet und keine Einnahmen erzielt. +
+ +
+ Harry Potter, Charaktere, Namen und zugehörige Kennzeichen sind Marken von und © Warner Bros. Entertainment Inc. und J.K. Rowling.
+ Die Verlagsrechte liegen bei Bloomsbury Publishing Plc sowie Carlsen Verlag GmbH. Alle Rechte verbleiben bei den jeweiligen Rechteinhabern. Es besteht keinerlei Absicht zur Verletzung von Marken- oder Urheberrechten.
+
+ Die Inhalte unserer Seiten wurden mit größter Sorgfalt erstellt. Für die Richtigkeit, Vollständigkeit und Aktualität der Inhalte können wir jedoch keine Gewähr übernehmen. Verlinkungen zu externen Webseiten Dritter unterliegen der Haftung der jeweiligen Betreiber. +
+ {% endif %} +