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 %} +
+ +
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/app/templates/finale.html b/app/templates/finale.html index 416238f..5bceb7b 100755 --- a/app/templates/finale.html +++ b/app/templates/finale.html @@ -1,58 +1,86 @@ {% extends "base.html" %} -{% block title %}{% if lang == 'en' %}🎉 The Grand Finale – Magical Quiz{% else %}🎉 Das große Finale – Magisches Quiz{% endif %}{% endblock %} +{% block title %}{% if lang == 'en' %}⚡ Hogwarts Report Card – Grand Finale{% else %}⚡ Das große Hogwarts-Zeugnis{% endif %}{% endblock %} {% block content %}
✨ -

🎉 Happy Birthday Schatz! 🎉

+

+ {% if lang == 'en' %}⚡ The Great Hogwarts Report Card ⚡{% else %}⚡ Das große Hogwarts-Zeugnis ⚡{% endif %} +

✨
+

{% if lang == 'en' %} - You secured {{ score }} out of {{ total }} possible House Points for your House! + You achieved {{ score }} out of {{ total }} House Points! {% else %} - Du hast {{ score }} von {{ total }} möglichen Hauspunkten für dein Haus gesichert! + Du hast {{ score }} von {{ total }} Hauspunkten erzielt! {% endif %}

+

+ ⏱️ {% if lang == 'en' %}Time needed:{% else %}Benötigte Zeit:{% endif %} {{ time_formatted }} +

-
-

- {% if lang == 'en' %} - 💛 Your Magical Reward: An Unforgettable Evening at Home 💛 - {% else %} - 💛 Dein magischer Gewinn: Ein unvergesslicher Abend Zuhause 💛 - {% 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.grade }} +
+

{{ tier.title }}

+

{{ tier.subtitle }}

+

{{ tier.description }}

+
+ + +
+

🏆 Trage dich in die Ruhmeshalle ein!

+

Völlig anonym – wähle deinen Zauberer-Spitznamen:

+ +
+
+ + +
+
+ + + + +
@@ -61,46 +89,285 @@ {% block scripts %} {% endblock %} diff --git a/app/templates/impressum.html b/app/templates/impressum.html new file mode 100644 index 0000000..9aa6fb4 --- /dev/null +++ b/app/templates/impressum.html @@ -0,0 +1,164 @@ +{% extends "base.html" %} + +{% block title %}{% if lang == 'en' %}⚖️ Legal Notice (Impressum){% else %}⚖️ Impressum{% endif %}{% endblock %} + +{% block content %} +
+ +
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/app/templates/index.html b/app/templates/index.html index 2da53f1..10898b9 100755 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -53,10 +53,11 @@