Implement Gamification (XP, 5 Ranks, 9 Badges) and Social Sharing (OpenGraph tags, Web Share API for exams & quiz)

This commit is contained in:
harald committed 2026-09-28 16:09:42 +02:00
1 parent 2f57c7d066
commit 64dfd30853
2 files changed
+438 -1

No files matched your search

+437
View File
@@ -16,6 +16,18 @@
<title>RhetorikMaster - Stilmittel Trainer, Quiz & Klausur-Spickzettel</title> <title>RhetorikMaster - Stilmittel Trainer, Quiz & Klausur-Spickzettel</title>
<!-- Open Graph & Social Media Meta Tags -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://rhetorikmaster.pansi.eu/">
<meta property="og:title" content="RhetorikMaster - Stilmittel Trainer, Quiz & Klausur-Spickzettel">
<meta property="og:description" content="Interaktiver Trainer für 23 rhetorische Stilmittel der Textanalyse mit Karteikarten, Klausursimulation, 230 Beispielen und Notenschlüssel.">
<meta property="og:image" content="https://rhetorikmaster.pansi.eu/icon-512.png">
<meta property="og:locale" content="de_DE">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="RhetorikMaster - Stilmittel Trainer & Klausur-Spickzettel">
<meta name="twitter:description" content="23 rhetorische Stilmittel sicher erkennen & anwenden mit interaktivem Quiz und Prüfungssimulation.">
<meta name="twitter:image" content="https://rhetorikmaster.pansi.eu/icon-512.png">
<!-- QR Code Library for offline mobile sync --> <!-- QR Code Library for offline mobile sync -->
<script src="/qrcode.min.js"></script> <script src="/qrcode.min.js"></script>
@@ -167,6 +179,19 @@
<span>App installieren</span> <span>App installieren</span>
</button> </button>
<!-- Achievements & Level Button -->
<button onclick="openAchievementsModal()" title="Erfolge, Rang & Trophäen ansehen" class="p-2 sm:px-3 sm:py-2 rounded-xl bg-amber-50 hover:bg-amber-100 text-amber-900 text-xs sm:text-sm font-bold border border-amber-200 transition flex items-center gap-1.5 shadow-sm">
<i class="fa-solid fa-trophy text-amber-500"></i>
<span class="hidden md:inline" id="userRankTitle">Novize</span>
<span class="px-1.5 py-0.5 rounded-full bg-amber-200/80 text-[10px] font-black text-amber-950" id="badgeCountPill">0/9</span>
</button>
<!-- Social Share Button -->
<button onclick="shareApp()" title="RhetorikMaster mit Freunden teilen" class="p-2 sm:px-3 sm:py-2 rounded-xl bg-slate-100 hover:bg-slate-200 text-slate-700 text-xs sm:text-sm font-bold border border-slate-200/80 transition flex items-center gap-1.5">
<i class="fa-solid fa-share-nodes text-indigo-600"></i>
<span class="hidden lg:inline">Teilen</span>
</button>
<!-- Sync & Export Button --> <!-- Sync & Export Button -->
<button onclick="openSyncModal()" title="Lernstand synchronisieren & sichern (QR-Code / JSON)" class="p-2 sm:px-3 sm:py-2 rounded-xl bg-slate-100 hover:bg-slate-200 text-slate-700 text-xs sm:text-sm font-bold border border-slate-200/80 transition flex items-center gap-1.5"> <button onclick="openSyncModal()" title="Lernstand synchronisieren & sichern (QR-Code / JSON)" class="p-2 sm:px-3 sm:py-2 rounded-xl bg-slate-100 hover:bg-slate-200 text-slate-700 text-xs sm:text-sm font-bold border border-slate-200/80 transition flex items-center gap-1.5">
<i class="fa-solid fa-arrows-rotate text-indigo-600"></i> <i class="fa-solid fa-arrows-rotate text-indigo-600"></i>
@@ -622,6 +647,10 @@
<!-- Action Buttons --> <!-- Action Buttons -->
<div class="flex flex-col sm:flex-row gap-3 justify-center pt-2"> <div class="flex flex-col sm:flex-row gap-3 justify-center pt-2">
<button onclick="shareExamResult()" class="px-6 py-3.5 rounded-2xl bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-700 hover:to-teal-700 text-white font-extrabold text-sm sm:text-base shadow-lg shadow-emerald-600/25 transition flex items-center justify-center gap-2">
<i class="fa-solid fa-share-nodes"></i>
<span>Zeugnis teilen</span>
</button>
<button onclick="startQuiz('exam')" class="px-6 py-3.5 rounded-2xl bg-indigo-600 hover:bg-indigo-700 text-white font-extrabold text-sm sm:text-base shadow-lg shadow-indigo-600/30 transition flex items-center justify-center gap-2"> <button onclick="startQuiz('exam')" class="px-6 py-3.5 rounded-2xl bg-indigo-600 hover:bg-indigo-700 text-white font-extrabold text-sm sm:text-base shadow-lg shadow-indigo-600/30 transition flex items-center justify-center gap-2">
<i class="fa-solid fa-rotate-right"></i> <i class="fa-solid fa-rotate-right"></i>
<span>Prüfung wiederholen</span> <span>Prüfung wiederholen</span>
@@ -668,6 +697,10 @@
</div> </div>
<div class="flex flex-col sm:flex-row gap-3 justify-center pt-2"> <div class="flex flex-col sm:flex-row gap-3 justify-center pt-2">
<button onclick="shareQuizResult()" class="px-6 py-3.5 rounded-2xl bg-gradient-to-r from-indigo-600 to-violet-600 hover:from-indigo-700 hover:to-violet-700 text-white font-extrabold text-sm sm:text-base shadow-lg shadow-indigo-600/25 transition flex items-center justify-center gap-2">
<i class="fa-solid fa-share-nodes"></i>
<span>Ergebnis teilen</span>
</button>
<button onclick="startQuiz(currentQuizMode)" class="px-6 py-3.5 rounded-2xl bg-indigo-600 hover:bg-indigo-700 text-white font-extrabold text-sm sm:text-base shadow-lg shadow-indigo-600/30 transition flex items-center justify-center gap-2"> <button onclick="startQuiz(currentQuizMode)" class="px-6 py-3.5 rounded-2xl bg-indigo-600 hover:bg-indigo-700 text-white font-extrabold text-sm sm:text-base shadow-lg shadow-indigo-600/30 transition flex items-center justify-center gap-2">
<i class="fa-solid fa-rotate-right"></i> <i class="fa-solid fa-rotate-right"></i>
<span>Nochmal spielen</span> <span>Nochmal spielen</span>
@@ -1049,6 +1082,71 @@
</div> </div>
</div> </div>
<!-- ACHIEVEMENTS & GAMIFICATION MODAL -->
<div id="achievementsModal" class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm z-50 hidden flex items-center justify-center p-4 no-print">
<div class="bg-white rounded-3xl max-w-xl w-full p-6 sm:p-8 shadow-2xl relative border border-slate-200 animate-in fade-in zoom-in-95 duration-150 max-h-[90vh] overflow-y-auto space-y-5">
<button onclick="closeAchievementsModal()" class="absolute top-5 right-5 w-9 h-9 rounded-full bg-slate-100 hover:bg-slate-200 text-slate-600 flex items-center justify-center transition">
<i class="fa-solid fa-xmark text-base"></i>
</button>
<div class="flex items-center gap-2">
<span class="text-xs font-bold px-2.5 py-0.5 rounded-full bg-amber-100 text-amber-900 border border-amber-200 uppercase tracking-wider">
Gamification & Motivation
</span>
</div>
<div>
<h3 class="text-2xl sm:text-3xl font-black text-slate-900">🏛️ Rang & Rhetorik-Erfolge</h3>
<p class="text-xs sm:text-sm text-slate-500 mt-0.5">Sammle XP durch Karteikarten, Quiz und Prüfungen, um alle 9 Trophäen freizuschalten.</p>
</div>
<!-- Current Rank Card -->
<div class="p-4 sm:p-5 bg-gradient-to-r from-amber-500 via-amber-600 to-orange-600 text-white rounded-2xl shadow-md space-y-3">
<div class="flex justify-between items-center">
<div>
<div class="text-[11px] font-extrabold uppercase tracking-wider text-amber-200">Dein aktueller Rang</div>
<div id="modalUserRankName" class="text-xl sm:text-2xl font-black tracking-tight">Rang 1: Novize</div>
</div>
<div class="w-12 h-12 rounded-2xl bg-white/20 backdrop-blur-sm flex items-center justify-center text-2xl">
<i id="modalUserRankIcon" class="fa-solid fa-seedling text-white"></i>
</div>
</div>
<div>
<div class="flex justify-between text-xs font-bold mb-1">
<span id="modalUserXpText">0 XP gesammelt</span>
<span id="modalNextRankText">500 XP bis Textanalytiker</span>
</div>
<div class="w-full bg-black/20 h-2.5 rounded-full overflow-hidden">
<div id="modalXpProgressBar" class="bg-white h-full transition-all duration-500 w-0"></div>
</div>
</div>
</div>
<!-- Badges Grid (9 Achievements) -->
<div class="space-y-3">
<div class="flex justify-between items-center border-b border-slate-100 pb-2">
<h4 class="text-xs sm:text-sm font-extrabold text-slate-700 uppercase tracking-wider">Trophäen-Kabinett (<span id="modalBadgesUnlockedCount">0</span> von 9 freigeschaltet)</h4>
<span class="text-xs text-slate-400 font-bold">Lokal gespeichert</span>
</div>
<div id="achievementsGrid" class="grid grid-cols-1 sm:grid-cols-2 gap-3 text-xs">
<!-- Dynamically populated badges -->
</div>
</div>
<div class="pt-2 border-t border-slate-100 flex justify-between items-center">
<button onclick="shareApp()" class="px-4 py-2 rounded-xl bg-slate-100 hover:bg-slate-200 text-slate-700 font-bold text-xs sm:text-sm transition flex items-center gap-1.5">
<i class="fa-solid fa-share-nodes text-indigo-600"></i>
<span>App teilen</span>
</button>
<button onclick="closeAchievementsModal()" class="px-5 py-2 rounded-xl bg-slate-900 hover:bg-slate-800 text-white font-bold text-xs sm:text-sm transition shadow-sm">
Schließen
</button>
</div>
</div>
</div>
<!-- Notification Toast --> <!-- Notification Toast -->
<div id="toastNotification" class="fixed bottom-6 right-6 z-50 transform translate-y-20 opacity-0 transition-all duration-300 pointer-events-none no-print"> <div id="toastNotification" class="fixed bottom-6 right-6 z-50 transform translate-y-20 opacity-0 transition-all duration-300 pointer-events-none no-print">
<div class="bg-slate-900 text-white px-5 py-3 rounded-2xl shadow-xl flex items-center gap-3 border border-slate-700 text-sm sm:text-base font-medium"> <div class="bg-slate-900 text-white px-5 py-3 rounded-2xl shadow-xl flex items-center gap-3 border border-slate-700 text-sm sm:text-base font-medium">
@@ -1955,6 +2053,7 @@
if (isMastered) { if (isMastered) {
setItemLevel(item.id, currentLvl + 1); setItemLevel(item.id, currentLvl + 1);
playTone('success'); playTone('success');
addXP(20, 'Karte gemeistert');
cardEl.classList.add('swipe-right'); cardEl.classList.add('swipe-right');
showToast(`„${item.term}“ aufgestiegen in Kasten ${Math.min(5, currentLvl + 1)}!`, 'fa-circle-up'); showToast(`„${item.term}“ aufgestiegen in Kasten ${Math.min(5, currentLvl + 1)}!`, 'fa-circle-up');
} else { } else {
@@ -2049,6 +2148,10 @@
utterance.rate = 0.95; utterance.rate = 0.95;
utterance.pitch = 1.0; utterance.pitch = 1.0;
window.speechSynthesis.speak(utterance); window.speechSynthesis.speak(utterance);
audioListenCount++;
localStorage.setItem('rhetorik_audio_count', audioListenCount.toString());
addXP(5);
} catch (err) { } catch (err) {
console.warn('SpeechSynthesis error:', err); console.warn('SpeechSynthesis error:', err);
} }
@@ -2572,6 +2675,13 @@
if (isCorrect) { if (isCorrect) {
quizScore += 100 + (quizStreak * 20); quizScore += 100 + (quizStreak * 20);
quizStreak++; quizStreak++;
if (currentQuizInputType === 'text') {
freitextCorrectCount++;
localStorage.setItem('rhetorik_freitext_count', freitextCorrectCount.toString());
addXP(25, 'Freitext richtig');
} else {
addXP(15, 'Quiz richtig');
}
if (quizStreak > quizMaxStreak) quizMaxStreak = quizStreak; if (quizStreak > quizMaxStreak) quizMaxStreak = quizStreak;
playTone('success'); playTone('success');
@@ -2712,6 +2822,15 @@
const gradeInfo = calculateExamGrade(correctCount, quizQuestions.length); const gradeInfo = calculateExamGrade(correctCount, quizQuestions.length);
playTone(gradeInfo.grade <= 3 ? 'win' : 'error'); playTone(gradeInfo.grade <= 3 ? 'win' : 'error');
// Grant Exam XP & Badges
if (gradeInfo.grade === 1) addXP(250, 'Note 1');
else if (gradeInfo.grade === 2) addXP(180, 'Note 2');
else if (gradeInfo.grade === 3) addXP(100, 'Note 3');
else if (gradeInfo.grade === 4) addXP(50, 'Note 4');
if (gradeInfo.grade <= 2) unlockBadge('exam_ace');
if (elapsedSec <= 300 && gradeInfo.grade <= 3) unlockBadge('speed_demon');
// Populate Exam Screen // Populate Exam Screen
document.getElementById('quizPlayArea').classList.add('hidden'); document.getElementById('quizPlayArea').classList.add('hidden');
document.getElementById('examTimerBar').classList.add('hidden'); document.getElementById('examTimerBar').classList.add('hidden');
@@ -3297,6 +3416,7 @@
const payload = JSON.parse(evt.target.result); const payload = JSON.parse(evt.target.result);
applySyncData(payload); applySyncData(payload);
closeSyncModal(); closeSyncModal();
closeAchievementsModal();
showToast('Lernstand aus JSON-Datei importiert!', 'fa-circle-check'); showToast('Lernstand aus JSON-Datei importiert!', 'fa-circle-check');
} catch (err) { } catch (err) {
alert('Ungültiges Backup-Format in der ausgewählten JSON-Datei.'); alert('Ungültiges Backup-Format in der ausgewählten JSON-Datei.');
@@ -3316,6 +3436,7 @@
} }
applySyncData(payload); applySyncData(payload);
closeSyncModal(); closeSyncModal();
closeAchievementsModal();
document.getElementById('syncImportText').value = ''; document.getElementById('syncImportText').value = '';
showToast('Lernstand erfolgreich aktualisiert!', 'fa-circle-check'); showToast('Lernstand erfolgreich aktualisiert!', 'fa-circle-check');
} }
@@ -3375,6 +3496,319 @@
} }
} }
// ==========================================
// GAMIFICATION, XP, BADGES & SOCIAL SHARING
// ==========================================
const ACHIEVEMENTS_DEF = [
{
id: "first_step",
title: "Erster Schritt",
desc: "Erstes Quiz absolviert oder 5 Karteikarten gelernt.",
icon: "fa-seedling",
color: "emerald",
xpReward: 50
},
{
id: "streak_10",
title: "Feuer & Flamme",
desc: "Einen 10er Streak im Quiz-Modus erreicht.",
icon: "fa-fire",
color: "amber",
xpReward: 100
},
{
id: "freitext_pro",
title: "Freitext-Virtuose",
desc: "5 Fragen ohne Multiple Choice per Freitext getippt.",
icon: "fa-keyboard",
color: "indigo",
xpReward: 150
},
{
id: "exam_ace",
title: "Klausur-Ass",
desc: "Eine Klausursimulation mit Note 1 oder 2 bestanden.",
icon: "fa-crown",
color: "amber",
xpReward: 200
},
{
id: "speed_demon",
title: "Speed-Denker",
desc: "Eine 15-Fragen-Prüfung in unter 5 Minuten absolviert.",
icon: "fa-bolt",
color: "violet",
xpReward: 120
},
{
id: "audio_listener",
title: "Hörversteher",
desc: "Mindestens 3 Zitate über die Sprachausgabe angehört.",
icon: "fa-headphones",
color: "blue",
xpReward: 60
},
{
id: "weakness_slayer",
title: "Fehlerjäger",
desc: "Alle offenen Schwachstellen im Training abgebaut.",
icon: "fa-shield-halved",
color: "rose",
xpReward: 150
},
{
id: "sound_master",
title: "Klangmeister",
desc: "Alle Klangfiguren in Leitner-Kasten 5 gemeistert.",
icon: "fa-music",
color: "cyan",
xpReward: 180
},
{
id: "grandmaster",
title: "Cicero der Neuzeit",
desc: "Alle 23 rhetorischen Stilmittel in Kasten 5 gemeistert.",
icon: "fa-trophy",
color: "amber",
xpReward: 500
}
];
const USER_RANKS = [
{ minXp: 0, name: "Novize", level: 1, icon: "fa-seedling" },
{ minXp: 500, name: "Textanalytiker", level: 2, icon: "fa-magnifying-glass" },
{ minXp: 1500, name: "Wortfechter", level: 3, icon: "fa-feather-pointed" },
{ minXp: 3000, name: "Meister der Rhetorik", level: 4, icon: "fa-award" },
{ minXp: 6000, name: "Cicero / Großmeister", level: 5, icon: "fa-crown" }
];
let userXP = parseInt(localStorage.getItem('rhetorik_user_xp') || '0', 10);
let unlockedBadges = JSON.parse(localStorage.getItem('rhetorik_unlocked_badges') || '[]');
let freitextCorrectCount = parseInt(localStorage.getItem('rhetorik_freitext_count') || '0', 10);
let audioListenCount = parseInt(localStorage.getItem('rhetorik_audio_count') || '0', 10);
function addXP(amount, reason = '') {
if (amount <= 0) return;
const prevRank = getCurrentRank(userXP);
userXP += amount;
localStorage.setItem('rhetorik_user_xp', userXP.toString());
updateGamificationHeader();
const newRank = getCurrentRank(userXP);
if (newRank.level > prevRank.level) {
playTone('win');
showToast(`🎉 RANG-AUFSTIEG! Du bist jetzt: ${newRank.name}!`, 'fa-crown');
}
checkAchievements();
}
function getCurrentRank(xp) {
let current = USER_RANKS[0];
for (let i = 0; i < USER_RANKS.length; i++) {
if (xp >= USER_RANKS[i].minXp) {
current = USER_RANKS[i];
} else {
break;
}
}
return current;
}
function getNextRank(xp) {
for (let i = 0; i < USER_RANKS.length; i++) {
if (xp < USER_RANKS[i].minXp) {
return USER_RANKS[i];
}
}
return null;
}
function updateGamificationHeader() {
const rank = getCurrentRank(userXP);
const titleEl = document.getElementById('userRankTitle');
if (titleEl) titleEl.innerText = rank.name;
const pillEl = document.getElementById('badgeCountPill');
if (pillEl) pillEl.innerText = `${unlockedBadges.length}/9`;
}
function unlockBadge(badgeId) {
if (unlockedBadges.includes(badgeId)) return;
const bDef = ACHIEVEMENTS_DEF.find(b => b.id === badgeId);
if (!bDef) return;
unlockedBadges.push(badgeId);
localStorage.setItem('rhetorik_unlocked_badges', JSON.stringify(unlockedBadges));
updateGamificationHeader();
playTone('win');
showToast(`🏆 Trophäe freigeschaltet: ${bDef.title}! (+${bDef.xpReward} XP)`, 'fa-trophy');
addXP(bDef.xpReward);
}
function checkAchievements() {
const masteredCount = STILMITTEL_DATA.filter(item => (cardLevels[item.id] || 1) >= 5).length;
const klangMastered = STILMITTEL_DATA.filter(item => item.category === 'klang' && (cardLevels[item.id] || 1) >= 5).length;
const totalKlang = STILMITTEL_DATA.filter(item => item.category === 'klang').length;
// 1. Erster Schritt
if (masteredCount >= 5 || userXP >= 50) {
unlockBadge('first_step');
}
// 2. Feuer & Flamme
if (quizMaxStreak >= 10) {
unlockBadge('streak_10');
}
// 3. Freitext-Virtuose
if (freitextCorrectCount >= 5) {
unlockBadge('freitext_pro');
}
// 6. Hörversteher
if (audioListenCount >= 3) {
unlockBadge('audio_listener');
}
// 7. Fehlerjäger
if (quizMistakes.length === 0 && userXP >= 300) {
unlockBadge('weakness_slayer');
}
// 8. Klangmeister
if (klangMastered === totalKlang && totalKlang > 0) {
unlockBadge('sound_master');
}
// 9. Großmeister
if (masteredCount >= STILMITTEL_DATA.length) {
unlockBadge('grandmaster');
}
}
function openAchievementsModal() {
const rank = getCurrentRank(userXP);
const nextRank = getNextRank(userXP);
document.getElementById('modalUserRankName').innerText = `Rang ${rank.level}: ${rank.name}`;
document.getElementById('modalUserRankIcon').className = `fa-solid ${rank.icon} text-white`;
const xpTextEl = document.getElementById('modalUserXpText');
const nextTextEl = document.getElementById('modalNextRankText');
const barEl = document.getElementById('modalXpProgressBar');
if (nextRank) {
xpTextEl.innerText = `${userXP} XP gesammelt`;
const diff = nextRank.minXp - rank.minXp;
const currentProgress = userXP - rank.minXp;
const percent = Math.min(100, Math.round((currentProgress / diff) * 100));
nextTextEl.innerText = `${nextRank.minXp - userXP} XP bis ${nextRank.name}`;
barEl.style.width = `${percent}%`;
} else {
xpTextEl.innerText = `${userXP} XP (Maximalrang erreicht)`;
nextTextEl.innerText = 'Cicero / Höchster Rang!';
barEl.style.width = '100%';
}
document.getElementById('modalBadgesUnlockedCount').innerText = unlockedBadges.length;
// Render 9 Badges
const grid = document.getElementById('achievementsGrid');
grid.innerHTML = '';
ACHIEVEMENTS_DEF.forEach(b => {
const isUnlocked = unlockedBadges.includes(b.id);
const card = document.createElement('div');
card.className = `p-3.5 rounded-2xl border transition flex items-start gap-3 ${
isUnlocked
? 'bg-amber-50/70 border-amber-300 shadow-sm text-slate-900'
: 'bg-slate-50 border-slate-200 text-slate-400 opacity-75'
}`;
card.innerHTML = `
<div class="w-10 h-10 rounded-xl flex items-center justify-center text-lg shrink-0 shadow-sm ${
isUnlocked ? 'bg-amber-500 text-white' : 'bg-slate-200 text-slate-400'
}">
<i class="fa-solid ${isUnlocked ? b.icon : 'fa-lock'}"></i>
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center justify-between gap-1">
<span class="font-extrabold text-sm ${isUnlocked ? 'text-slate-900' : 'text-slate-500'} truncate">${b.title}</span>
${isUnlocked ? '<span class="text-[10px] font-black uppercase text-emerald-600 bg-emerald-100 px-1.5 py-0.5 rounded-md">Frei</span>' : '<span class="text-[10px] font-mono text-slate-400">+' + b.xpReward + ' XP</span>'}
</div>
<p class="text-[11px] leading-snug mt-0.5 ${isUnlocked ? 'text-slate-600' : 'text-slate-400'}">${b.desc}</p>
</div>
`;
grid.appendChild(card);
});
playTone('flip');
document.getElementById('achievementsModal').classList.remove('hidden');
}
function closeAchievementsModal() {
document.getElementById('achievementsModal').classList.add('hidden');
}
// SOCIAL SHARING FUNCTIONS
function shareApp() {
const shareData = {
title: 'RhetorikMaster - Stilmittel Trainer & Klausur-Spickzettel',
text: '🏛️ Meistere 23 rhetorische Stilmittel der Textanalyse mit Karteikarten, interaktivem Quiz und Klausursimulation!',
url: 'https://rhetorikmaster.pansi.eu/'
};
if (navigator.share) {
navigator.share(shareData).catch(() => {});
} else {
navigator.clipboard.writeText(`${shareData.text} ${shareData.url}`).then(() => {
showToast('Empfehlungslink in die Zwischenablage kopiert!', 'fa-share-nodes');
});
}
}
function shareExamResult() {
const gradeTitle = document.getElementById('examGradeTitle')?.innerText || 'Note: 1';
const scoreText = document.getElementById('examScoreDisplay')?.innerText || '14 / 15';
const percentText = document.getElementById('examPercentDisplay')?.innerText || '93%';
const shareData = {
title: 'Mein Klausur-Ergebnis im RhetorikMaster',
text: `🎓 Ich habe gerade die RhetorikMaster Klausursimulation mit ${gradeTitle} (${scoreText}, ${percentText}) bestanden! 🏛️ Teste dein Stilmittel-Wissen für die Textanalyse:`,
url: 'https://rhetorikmaster.pansi.eu/'
};
if (navigator.share) {
navigator.share(shareData).catch(() => {});
} else {
navigator.clipboard.writeText(`${shareData.text} ${shareData.url}`).then(() => {
showToast('Klausur-Zeugnis zum Teilen kopiert!', 'fa-copy');
});
}
}
function shareQuizResult() {
const scoreText = document.getElementById('resultScore')?.innerText || '0 Pkt';
const streakText = document.getElementById('resultMaxStreak')?.innerText || '0';
const shareData = {
title: 'Mein Highscore im RhetorikMaster',
text: `🏆 Ich habe im RhetorikMaster Quiz ${scoreText} und einen Streak von ${streakText} erreicht! Schlägst du mein Ergebnis?`,
url: 'https://rhetorikmaster.pansi.eu/'
};
if (navigator.share) {
navigator.share(shareData).catch(() => {});
} else {
navigator.clipboard.writeText(`${shareData.text} ${shareData.url}`).then(() => {
showToast('Quiz-Ergebnis zum Teilen kopiert!', 'fa-copy');
});
}
}
// Keyboard Shortcuts // Keyboard Shortcuts
window.addEventListener('keydown', (e) => { window.addEventListener('keydown', (e) => {
if (document.activeElement && document.activeElement.tagName === 'INPUT') { if (document.activeElement && document.activeElement.tagName === 'INPUT') {
@@ -3403,6 +3837,7 @@
if (e.key === 'Escape') { if (e.key === 'Escape') {
closeDetailModal(); closeDetailModal();
closeSyncModal(); closeSyncModal();
closeAchievementsModal();
cancelIncomingSync(); cancelIncomingSync();
return; return;
} }
@@ -3501,6 +3936,8 @@
applyCardFilters(); applyCardFilters();
updateMasteredHeader(); updateMasteredHeader();
renderPrintCheatsheet(); renderPrintCheatsheet();
updateGamificationHeader();
checkAchievements();
checkUrlSync(); checkUrlSync();
}; };
</script> </script>
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE_NAME = 'rhetorikmaster-v8'; const CACHE_NAME = 'rhetorikmaster-v9';
const STATIC_ASSETS = [ const STATIC_ASSETS = [
'/', '/',
'/index.html', '/index.html',