374 lines
17 KiB
HTML
Executable File
374 lines
17 KiB
HTML
Executable File
{% extends "base.html" %}
|
||
|
||
{% block title %}{% if lang == 'en' %}⚡ Hogwarts Report Card – Grand Finale{% else %}⚡ Das große Hogwarts-Zeugnis{% endif %}{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="quiz-wrapper">
|
||
<div class="card finale-card">
|
||
<div class="finale-header">
|
||
<span class="sparkle-icon">✨</span>
|
||
<h1 class="glow-title" id="finaleTitle">
|
||
{% if lang == 'en' %}⚡ The Great Hogwarts Report Card ⚡{% else %}⚡ Das große Hogwarts-Zeugnis ⚡{% endif %}
|
||
</h1>
|
||
<span class="sparkle-icon">✨</span>
|
||
</div>
|
||
|
||
<!-- Score & Time Stats -->
|
||
<div class="score-banner">
|
||
<p class="score-highlight" id="finaleScoreText">
|
||
{% if lang == 'en' %}
|
||
You achieved <span class="gold-text">{{ score }}</span> out of <span class="gold-text">{{ total }}</span> House Points!
|
||
{% else %}
|
||
Du hast <span class="gold-text">{{ score }}</span> von <span class="gold-text">{{ total }}</span> Hauspunkten erzielt!
|
||
{% endif %}
|
||
</p>
|
||
<p class="time-highlight" id="finaleTimeText">
|
||
⏱️ {% if lang == 'en' %}Time needed:{% else %}Benötigte Zeit:{% endif %} <strong class="gold-text">{{ time_formatted }}</strong>
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Tiered Wizard Evaluation -->
|
||
<div class="reward-box tier-box" id="tierBox">
|
||
<div class="tier-badge-container">
|
||
<span id="tierGrade" class="tier-grade-badge {{ tier.badge_class }}">{{ tier.grade }}</span>
|
||
</div>
|
||
<h2 id="tierTitle">{{ tier.title }}</h2>
|
||
<p class="tier-subtitle" id="tierSubtitle">{{ tier.subtitle }}</p>
|
||
<p class="reward-description" id="tierDesc">{{ tier.description }}</p>
|
||
</div>
|
||
|
||
<!-- Leaderboard Submission Card -->
|
||
<div class="card leaderboard-entry-card" id="lbFormCard">
|
||
<h3 id="lbCardTitle">🏆 Trage dich in die Ruhmeshalle ein!</h3>
|
||
<p class="subtitle" id="lbCardSub">Völlig anonym – wähle deinen Zauberer-Spitznamen:</p>
|
||
|
||
<form id="leaderboardForm" class="magic-form" onsubmit="submitScoreToLeaderboard(event)" style="margin-top: 12px;">
|
||
<div class="input-with-btn">
|
||
<input type="text" id="nicknameInput" class="input-text" placeholder="z. B. Fliegender_Besen oder Lord_Voldi" maxlength="30" required autocomplete="off">
|
||
<button type="submit" id="lbSubmitBtn" class="btn btn-primary">
|
||
📜 Verewigen
|
||
</button>
|
||
</div>
|
||
</form>
|
||
|
||
<div id="lbFeedback" class="feedback-box hidden" style="margin-top: 10px;"></div>
|
||
|
||
<!-- Mini Leaderboard Table -->
|
||
<div id="miniLbContainer" class="mini-lb-table-wrapper hidden" style="margin-top: 15px;">
|
||
<h4 id="miniLbTitle" style="color: var(--gold); margin-bottom: 8px;">Top Zauberer heute:</h4>
|
||
<div class="table-responsive">
|
||
<table class="lb-table">
|
||
<thead>
|
||
<tr>
|
||
<th>#</th>
|
||
<th id="thNick">Zauberer</th>
|
||
<th id="thPoints">Punkte</th>
|
||
<th id="thTime">Zeit</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="miniLbBody"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="btn-group finale-actions">
|
||
<a href="/" class="btn btn-primary" id="playAgainBtn">
|
||
{% if lang == 'en' %}🔄 Play Again{% else %}🔄 Noch einmal spielen{% endif %}
|
||
</a>
|
||
<a href="/bestenliste" class="btn btn-secondary" id="hallOfFameBtn">
|
||
{% if lang == 'en' %}🏆 Hall of Fame{% else %}🏆 Zur Bestenliste{% endif %}
|
||
</a>
|
||
<a href="/vorschlag" class="btn btn-secondary" id="suggestBtn">
|
||
{% if lang == 'en' %}✍️ Submit Question{% else %}✍️ Frage vorschlagen{% endif %}
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block scripts %}
|
||
<script>
|
||
const score = {{ score }};
|
||
const total = {{ total }};
|
||
const timeSec = {{ time_seconds }};
|
||
let scoreAlreadySubmitted = false;
|
||
|
||
const FINALE_I18N = {
|
||
de: {
|
||
title: "⚡ Das große Hogwarts-Zeugnis ⚡",
|
||
scoreText: `Du hast <span class="gold-text">${score}</span> von <span class="gold-text">${total}</span> Hauspunkten erzielt!`,
|
||
timeText: `⏱️ Benötigte Zeit: <strong class="gold-text">${formatSecondsDe(timeSec)}</strong>`,
|
||
lbTitle: "🏆 Trage dich in die Ruhmeshalle ein!",
|
||
lbSub: "Völlig anonym – wähle deinen Zauberer-Spitznamen:",
|
||
lbPlaceholder: "z. B. Fliegender_Besen oder Lord_Voldi",
|
||
lbBtn: "📜 Verewigen",
|
||
lbBtnSending: "✨ Wird verewigt...",
|
||
miniLbTitle: "Top Zauberer:",
|
||
thNick: "Zauberer",
|
||
thPoints: "Punkte",
|
||
thTime: "Zeit",
|
||
playAgain: "🔄 Noch einmal spielen",
|
||
hallOfFame: "🏆 Zur Bestenliste",
|
||
suggest: "✍️ Frage vorschlagen",
|
||
rankSuccess: (rank) => `✨ Glückwunsch! Du stehst auf **Platz #${rank}** der Ruhmeshalle!`
|
||
},
|
||
en: {
|
||
title: "⚡ The Great Hogwarts Report Card ⚡",
|
||
scoreText: `You achieved <span class="gold-text">${score}</span> out of <span class="gold-text">${total}</span> House Points!`,
|
||
timeText: `⏱️ Time needed: <strong class="gold-text">${formatSecondsEn(timeSec)}</strong>`,
|
||
lbTitle: "🏆 Enter the Hogwarts Hall of Fame!",
|
||
lbSub: "Completely anonymous – choose your wizard nickname:",
|
||
lbPlaceholder: "e.g. Flying_Broom or Lord_Voldi",
|
||
lbBtn: "📜 Enter Score",
|
||
lbBtnSending: "✨ Entering score...",
|
||
miniLbTitle: "Top Wizards:",
|
||
thNick: "Wizard",
|
||
thPoints: "Points",
|
||
thTime: "Time",
|
||
playAgain: "🔄 Play Again",
|
||
hallOfFame: "🏆 Hall of Fame",
|
||
suggest: "✍️ Submit Question",
|
||
rankSuccess: (rank) => `✨ Congratulations! You are **Rank #${rank}** in the Hall of Fame!`
|
||
}
|
||
};
|
||
|
||
function formatSecondsDe(sec) {
|
||
if (sec >= 60) {
|
||
const m = Math.floor(sec / 60);
|
||
const s = sec % 60;
|
||
return `${m} Min. ${s} Sek.`;
|
||
}
|
||
return `${sec} Sek.`;
|
||
}
|
||
|
||
function formatSecondsEn(sec) {
|
||
if (sec >= 60) {
|
||
const m = Math.floor(sec / 60);
|
||
const s = sec % 60;
|
||
return `${m} min. ${s} sec.`;
|
||
}
|
||
return `${sec} sec.`;
|
||
}
|
||
|
||
const TIERS_DATA = {
|
||
10: {
|
||
grade: "O",
|
||
badge_class: "tier-outstanding",
|
||
de: {
|
||
title: "🧙♂️ Großmeister der Zauberei",
|
||
sub: "Note: O (Ohnegleichen)",
|
||
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!"
|
||
},
|
||
en: {
|
||
title: "🧙♂️ Grandmaster of Wizardry",
|
||
sub: "Grade: O (Outstanding)",
|
||
desc: "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!"
|
||
}
|
||
},
|
||
8: {
|
||
grade: "E",
|
||
badge_class: "tier-exceeds",
|
||
de: {
|
||
title: "⚡ Meister-Hexe / Zauberer",
|
||
sub: "Note: E (Erwartungen übertroffen)",
|
||
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."
|
||
},
|
||
en: {
|
||
title: "⚡ Master Witch / Wizard",
|
||
sub: "Grade: E (Exceeds Expectations)",
|
||
desc: "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."
|
||
}
|
||
},
|
||
5: {
|
||
grade: "A",
|
||
badge_class: "tier-acceptable",
|
||
de: {
|
||
title: "📜 Solider Hogwarts-Schüler",
|
||
sub: "Note: A (Annehmbar)",
|
||
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!"
|
||
},
|
||
en: {
|
||
title: "📜 Solid Hogwarts Student",
|
||
sub: "Grade: A (Acceptable)",
|
||
desc: "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!"
|
||
}
|
||
},
|
||
2: {
|
||
grade: "M",
|
||
badge_class: "tier-poor",
|
||
de: {
|
||
title: "🧹 Quaffel-Fänger in Not",
|
||
sub: "Note: M (Mies)",
|
||
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!"
|
||
},
|
||
en: {
|
||
title: "🧹 Quaffle Catcher in Distress",
|
||
sub: "Grade: P (Poor)",
|
||
desc: "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!"
|
||
}
|
||
},
|
||
0: {
|
||
grade: "T",
|
||
badge_class: "tier-troll",
|
||
de: {
|
||
title: "🧌 Bergtroll im Kerker",
|
||
sub: "Note: 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!"
|
||
},
|
||
en: {
|
||
title: "🧌 Mountain Troll in the Dungeon",
|
||
sub: "Grade: T (Troll)",
|
||
desc: "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!"
|
||
}
|
||
}
|
||
};
|
||
|
||
function getTierForScore(s) {
|
||
if (s >= 10) return TIERS_DATA[10];
|
||
if (s >= 8) return TIERS_DATA[8];
|
||
if (s >= 5) return TIERS_DATA[5];
|
||
if (s >= 2) return TIERS_DATA[2];
|
||
return TIERS_DATA[0];
|
||
}
|
||
|
||
function onLanguageChange(lang) {
|
||
const isDe = (lang !== "en");
|
||
const t = isDe ? FINALE_I18N.de : FINALE_I18N.en;
|
||
|
||
const title = document.getElementById("finaleTitle");
|
||
const scoreText = document.getElementById("finaleScoreText");
|
||
const timeText = document.getElementById("finaleTimeText");
|
||
const lbTitle = document.getElementById("lbCardTitle");
|
||
const lbSub = document.getElementById("lbCardSub");
|
||
const nicknameInput = document.getElementById("nicknameInput");
|
||
const lbSubmitBtn = document.getElementById("lbSubmitBtn");
|
||
const miniLbTitle = document.getElementById("miniLbTitle");
|
||
const thNick = document.getElementById("thNick");
|
||
const thPoints = document.getElementById("thPoints");
|
||
const thTime = document.getElementById("thTime");
|
||
const playAgain = document.getElementById("playAgainBtn");
|
||
const hallOfFame = document.getElementById("hallOfFameBtn");
|
||
const suggest = document.getElementById("suggestBtn");
|
||
|
||
if (title) title.innerText = t.title;
|
||
if (scoreText) scoreText.innerHTML = t.scoreText;
|
||
if (timeText) timeText.innerHTML = t.timeText;
|
||
if (lbTitle) lbTitle.innerText = t.lbTitle;
|
||
if (lbSub) lbSub.innerText = t.lbSub;
|
||
if (nicknameInput) nicknameInput.placeholder = t.lbPlaceholder;
|
||
if (lbSubmitBtn && !lbSubmitBtn.disabled) lbSubmitBtn.innerText = t.lbBtn;
|
||
if (miniLbTitle) miniLbTitle.innerText = t.miniLbTitle;
|
||
if (thNick) thNick.innerText = t.thNick;
|
||
if (thPoints) thPoints.innerText = t.thPoints;
|
||
if (thTime) thTime.innerText = t.thTime;
|
||
if (playAgain) playAgain.innerText = t.playAgain;
|
||
if (hallOfFame) hallOfFame.innerText = t.hallOfFame;
|
||
if (suggest) suggest.innerText = t.suggest;
|
||
|
||
// Tier evaluation
|
||
const tier = getTierForScore(score);
|
||
const tierContent = isDe ? tier.de : tier.en;
|
||
const tierGrade = document.getElementById("tierGrade");
|
||
const tierTitle = document.getElementById("tierTitle");
|
||
const tierSubtitle = document.getElementById("tierSubtitle");
|
||
const tierDesc = document.getElementById("tierDesc");
|
||
|
||
if (tierGrade) {
|
||
tierGrade.className = `tier-grade-badge ${tier.badge_class}`;
|
||
tierGrade.innerText = tier.grade;
|
||
}
|
||
if (tierTitle) tierTitle.innerText = tierContent.title;
|
||
if (tierSubtitle) tierSubtitle.innerText = tierContent.sub;
|
||
if (tierDesc) tierDesc.innerText = tierContent.desc;
|
||
}
|
||
|
||
async function submitScoreToLeaderboard(event) {
|
||
event.preventDefault();
|
||
if (scoreAlreadySubmitted) return;
|
||
|
||
const isDe = (currentLang !== "en");
|
||
const t = isDe ? FINALE_I18N.de : FINALE_I18N.en;
|
||
const nickname = document.getElementById("nicknameInput").value.trim();
|
||
const submitBtn = document.getElementById("lbSubmitBtn");
|
||
const feedback = document.getElementById("lbFeedback");
|
||
|
||
submitBtn.disabled = true;
|
||
submitBtn.innerText = t.lbBtnSending;
|
||
|
||
try {
|
||
const res = await fetch("/api/leaderboard", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({
|
||
nickname: nickname,
|
||
score: score,
|
||
total_questions: total,
|
||
time_seconds: timeSec,
|
||
difficulty: "all",
|
||
lang: currentLang
|
||
})
|
||
});
|
||
const data = await res.json();
|
||
|
||
if (res.ok && data.success) {
|
||
scoreAlreadySubmitted = true;
|
||
feedback.className = "feedback-box feedback-success";
|
||
feedback.innerHTML = `
|
||
<div style="background: rgba(46, 204, 113, 0.2); border: 1px solid #2ecc71; color: #55efc4; padding: 12px; border-radius: 8px;">
|
||
✨ <strong>${data.nickname}</strong>: ${t.rankSuccess(data.rank)}
|
||
</div>
|
||
`;
|
||
feedback.classList.remove("hidden");
|
||
document.getElementById("nicknameInput").disabled = true;
|
||
submitBtn.innerText = isDe ? "✅ Eingetragen" : "✅ Entered";
|
||
loadMiniLeaderboard();
|
||
} else {
|
||
feedback.className = "feedback-box alert alert-danger";
|
||
feedback.innerText = data.detail || (isDe ? "Fehler beim Eintragen." : "Error entering score.");
|
||
feedback.classList.remove("hidden");
|
||
submitBtn.disabled = false;
|
||
submitBtn.innerText = t.lbBtn;
|
||
}
|
||
} catch (err) {
|
||
feedback.className = "feedback-box alert alert-danger";
|
||
feedback.innerText = "Netzwerkfehler: " + err.message;
|
||
feedback.classList.remove("hidden");
|
||
submitBtn.disabled = false;
|
||
submitBtn.innerText = t.lbBtn;
|
||
}
|
||
}
|
||
|
||
async function loadMiniLeaderboard() {
|
||
try {
|
||
const res = await fetch("/api/leaderboard?limit=5");
|
||
const entries = await res.json();
|
||
const body = document.getElementById("miniLbBody");
|
||
const container = document.getElementById("miniLbContainer");
|
||
|
||
body.innerHTML = "";
|
||
const medals = ["🥇", "🥈", "🥉"];
|
||
entries.forEach(e => {
|
||
const tr = document.createElement("tr");
|
||
const rankDisplay = medals[e.rank - 1] || `${e.rank}.`;
|
||
tr.innerHTML = `
|
||
<td><strong>${rankDisplay}</strong></td>
|
||
<td>${e.nickname}</td>
|
||
<td><span class="gold-text">${e.score}/${e.total_questions}</span></td>
|
||
<td>${e.time_formatted}</td>
|
||
`;
|
||
body.appendChild(tr);
|
||
});
|
||
container.classList.remove("hidden");
|
||
} catch (err) {
|
||
console.error("Could not load mini leaderboard", err);
|
||
}
|
||
}
|
||
|
||
document.addEventListener("DOMContentLoaded", () => {
|
||
onLanguageChange(currentLang);
|
||
loadMiniLeaderboard();
|
||
});
|
||
</script>
|
||
{% endblock %}
|