Implement Gamification (XP, 5 Ranks, 9 Badges) and Social Sharing (OpenGraph tags, Web Share API for exams & quiz)
This commit is contained in:
1 parent
2f57c7d066
commit
64dfd30853
2 files changed
+439
-2
No files matched your search
@@ -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,7 +179,20 @@
|
|||||||
<span>App installieren</span>
|
<span>App installieren</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Sync & Export 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 -->
|
||||||
<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>
|
||||||
<span class="hidden sm:inline">Sync</span>
|
<span class="hidden sm:inline">Sync</span>
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in new issue
Block a user