4567 lines
232 KiB
HTML
4567 lines
232 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de" class="h-full">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, viewport-fit=cover">
|
||
<!-- PWA & Mobile Meta Tags -->
|
||
<meta name="description" content="Interaktiver Trainer für rhetorische Stilmittel der Textanalyse mit Karteikarten, Leitner-System, Quiz, Schwachstellen-Trainer und Spickzettel-Druck.">
|
||
<meta name="theme-color" content="#4f46e5">
|
||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||
<meta name="apple-mobile-web-app-title" content="RhetorikMaster">
|
||
|
||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||
<link rel="apple-touch-icon" href="/icon-192.png">
|
||
<link rel="manifest" href="/manifest.json">
|
||
|
||
<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 -->
|
||
<script src="/qrcode.min.js"></script>
|
||
|
||
<!-- Tailwind CSS -->
|
||
<script src="https://cdn.tailwindcss.com"></script>
|
||
|
||
<!-- FontAwesome Icons -->
|
||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||
|
||
<!-- Google Fonts Inter -->
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||
|
||
<script>
|
||
tailwind.config = {
|
||
theme: {
|
||
extend: {
|
||
fontFamily: {
|
||
sans: ['"Plus Jakarta Sans"', 'system-ui', '-apple-system', 'sans-serif'],
|
||
},
|
||
colors: {
|
||
brand: {
|
||
50: '#eef2ff',
|
||
100: '#e0e7ff',
|
||
500: '#6366f1',
|
||
600: '#4f46e5',
|
||
700: '#4338ca',
|
||
800: '#3730a3',
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style>
|
||
/* 3D Flip Card Styles */
|
||
.perspective-1000 {
|
||
perspective: 1000px;
|
||
}
|
||
.transform-style-3d {
|
||
transform-style: preserve-3d;
|
||
}
|
||
.backface-hidden {
|
||
backface-visibility: hidden;
|
||
-webkit-backface-visibility: hidden;
|
||
}
|
||
.rotate-y-180 {
|
||
transform: rotateY(180deg);
|
||
}
|
||
|
||
/* Custom scrollbar */
|
||
::-webkit-scrollbar {
|
||
width: 6px;
|
||
height: 6px;
|
||
}
|
||
::-webkit-scrollbar-track {
|
||
background: #f1f5f9;
|
||
}
|
||
::-webkit-scrollbar-thumb {
|
||
background: #cbd5e1;
|
||
border-radius: 9999px;
|
||
}
|
||
::-webkit-scrollbar-thumb:hover {
|
||
background: #94a3b8;
|
||
}
|
||
|
||
/* Swipe animations */
|
||
.swipe-left {
|
||
transform: translateX(-150%) rotate(-20deg) !important;
|
||
opacity: 0 !important;
|
||
transition: all 0.35s ease-out !important;
|
||
}
|
||
.swipe-right {
|
||
transform: translateX(150%) rotate(20deg) !important;
|
||
opacity: 0 !important;
|
||
transition: all 0.35s ease-out !important;
|
||
}
|
||
|
||
/* Print Stylesheet for Klausur-Spickzettel */
|
||
@media print {
|
||
body {
|
||
background: #ffffff !important;
|
||
color: #000000 !important;
|
||
font-size: 10pt !important;
|
||
}
|
||
header, nav, #view-cards, #view-quiz, #view-matcher, #toastNotification, #detailModal, .no-print {
|
||
display: none !important;
|
||
}
|
||
#view-glossary {
|
||
display: none !important;
|
||
}
|
||
#printCheatsheetSection {
|
||
display: block !important;
|
||
}
|
||
.page-break {
|
||
page-break-after: always;
|
||
}
|
||
table {
|
||
border-collapse: collapse !important;
|
||
width: 100% !important;
|
||
}
|
||
th, td {
|
||
border: 1px solid #cbd5e1 !important;
|
||
padding: 6px 8px !important;
|
||
}
|
||
th {
|
||
background-color: #f1f5f9 !important;
|
||
}
|
||
}
|
||
@media screen {
|
||
#printCheatsheetSection {
|
||
display: none;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body class="bg-slate-50 text-slate-800 font-sans min-h-full flex flex-col selection:bg-indigo-100 selection:text-indigo-800">
|
||
|
||
<header class="bg-white border-b border-slate-200 sticky top-0 z-40 shadow-sm no-print">
|
||
<div class="max-w-6xl mx-auto px-3.5 sm:px-6 lg:px-8">
|
||
<div class="flex items-center justify-between h-16 sm:h-20 gap-2">
|
||
<!-- Brand Logo -->
|
||
<div class="flex items-center space-x-2.5 sm:space-x-3 cursor-pointer select-none" onclick="switchTab('cards')">
|
||
<div class="w-10 h-10 rounded-2xl bg-gradient-to-tr from-indigo-600 to-violet-600 flex items-center justify-center text-white shadow-md shadow-indigo-200">
|
||
<i class="fa-solid fa-feather-pointed text-lg"></i>
|
||
</div>
|
||
<div>
|
||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||
<span class="text-xl sm:text-2xl font-black tracking-tight text-slate-900">Rhetorik<span class="text-indigo-600">Master</span></span>
|
||
<span class="px-2 py-0.5 text-[11px] sm:text-xs font-bold bg-indigo-50 text-indigo-700 rounded-full border border-indigo-100">Pro</span>
|
||
</div>
|
||
<p class="text-xs text-slate-500 hidden sm:block">Stilmittel sicher erkennen, lernen & anwenden</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Global Status / Actions -->
|
||
<div class="flex items-center gap-1.5 sm:gap-3">
|
||
<!-- Sound Toggle -->
|
||
<button id="soundToggleBtn" onclick="toggleSound()" title="Soundeffekte umschalten (M)" 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 id="soundIcon" class="fa-solid fa-volume-high text-indigo-600"></i>
|
||
<span class="hidden md:inline" id="soundLabel">Ton An</span>
|
||
</button>
|
||
|
||
<!-- PWA Install Button -->
|
||
<button id="pwaInstallBtn" onclick="installPWA()" class="hidden px-3 py-2 rounded-xl bg-indigo-50 hover:bg-indigo-100 text-indigo-700 text-xs sm:text-sm font-bold border border-indigo-200 transition items-center gap-1.5">
|
||
<i class="fa-solid fa-download text-indigo-600"></i>
|
||
<span>App installieren</span>
|
||
</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">
|
||
<i class="fa-solid fa-arrows-rotate text-indigo-600"></i>
|
||
<span class="hidden sm:inline">Sync</span>
|
||
</button>
|
||
|
||
<!-- Print Cheatsheet -->
|
||
<button onclick="printCheatsheet()" title="Klausur-Spickzettel drucken / als PDF (P)" 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-print text-indigo-600"></i>
|
||
<span class="hidden sm:inline">Spickzettel</span>
|
||
</button>
|
||
|
||
<!-- Mastered Stat Badge -->
|
||
<div class="bg-indigo-50/90 px-2.5 sm:px-3.5 py-1.5 sm:py-2 rounded-xl flex items-center space-x-1.5 sm:space-x-2 text-xs sm:text-sm font-bold text-indigo-900 border border-indigo-100/90">
|
||
<i class="fa-solid fa-graduation-cap text-indigo-600 text-sm"></i>
|
||
<span id="globalMasteredStat">0 / 23 gelernt</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Navigation Tabs -->
|
||
<nav class="flex space-x-2 overflow-x-auto py-2.5 scrollbar-none border-t border-slate-100 text-xs sm:text-sm font-bold">
|
||
<button onclick="switchTab('cards')" id="tab-btn-cards" class="tab-btn active px-3.5 sm:px-4 py-2 rounded-xl flex items-center gap-2 transition duration-150 whitespace-nowrap bg-indigo-50 text-indigo-700 font-bold shadow-sm">
|
||
<i class="fa-regular fa-clone text-sm"></i>
|
||
<span>Karteikarten</span>
|
||
</button>
|
||
<button onclick="switchTab('quiz')" id="tab-btn-quiz" class="tab-btn px-3.5 sm:px-4 py-2 rounded-xl flex items-center gap-2 transition duration-150 whitespace-nowrap text-slate-600 hover:text-slate-900 hover:bg-slate-100 font-semibold">
|
||
<i class="fa-solid fa-brain text-sm"></i>
|
||
<span>Quiz-Modus</span>
|
||
</button>
|
||
<button onclick="switchTab('matcher')" id="tab-btn-matcher" class="tab-btn px-3.5 sm:px-4 py-2 rounded-xl flex items-center gap-2 transition duration-150 whitespace-nowrap text-slate-600 hover:text-slate-900 hover:bg-slate-100 font-semibold">
|
||
<i class="fa-solid fa-puzzle-piece text-sm"></i>
|
||
<span>Phrasen-Matcher</span>
|
||
</button>
|
||
<button onclick="switchTab('glossary')" id="tab-btn-glossary" class="tab-btn px-3.5 sm:px-4 py-2 rounded-xl flex items-center gap-2 transition duration-150 whitespace-nowrap text-slate-600 hover:text-slate-900 hover:bg-slate-100 font-semibold">
|
||
<i class="fa-solid fa-book-open text-sm"></i>
|
||
<span>Glossar & Suche</span>
|
||
</button>
|
||
</nav>
|
||
</div>
|
||
</header>
|
||
|
||
<main class="flex-1 max-w-6xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-6 no-print">
|
||
|
||
<!-- ========================================== -->
|
||
<!-- VIEW 1: FLASHCARDS (Leitner 5-Box-System) -->
|
||
<!-- ========================================== -->
|
||
<section id="view-cards" class="space-y-4 sm:space-y-5">
|
||
<!-- Filter Bar: Categories & Algorithm Selectors -->
|
||
<div class="bg-white p-4 sm:p-5 rounded-2xl border border-slate-200/80 shadow-sm space-y-3.5">
|
||
<div class="flex flex-col lg:flex-row justify-between items-start lg:items-center gap-3 sm:gap-4">
|
||
<div>
|
||
<div class="flex items-center gap-2.5 flex-wrap">
|
||
<h2 id="cardSectionTitle" class="text-base sm:text-xl font-extrabold text-slate-900 flex items-center gap-2">
|
||
<i class="fa-solid fa-brain text-indigo-600"></i>
|
||
<span>FSRS-4.5 Karteikasten</span>
|
||
</h2>
|
||
<span id="algoHeaderBadge" class="px-2.5 py-0.5 rounded-full bg-indigo-50 border border-indigo-200 text-indigo-700 font-extrabold text-[11px] uppercase tracking-wider">Spaced Repetition</span>
|
||
</div>
|
||
<p id="cardSectionSubtitle" class="text-xs sm:text-sm text-slate-500 mt-1">
|
||
Modernes 17-Parameter DSR-Modell (Retrievability, Stability, Difficulty) – 20–30 % weniger Wiederholungen bei 90 % Behaltungsquote.
|
||
</p>
|
||
</div>
|
||
|
||
<div class="flex flex-wrap items-center gap-2 w-full lg:w-auto justify-between lg:justify-end">
|
||
<!-- Algorithm Toggle -->
|
||
<div class="inline-flex p-1 bg-slate-100 rounded-xl border border-slate-200 text-xs sm:text-sm font-bold shadow-inner">
|
||
<button id="algoFsrsBtn" onclick="setSpacedRepetitionAlgo('fsrs')" class="px-2.5 sm:px-3 py-1.5 rounded-lg bg-white text-indigo-700 shadow-sm transition font-bold flex items-center gap-1.5">
|
||
<i class="fa-solid fa-bolt text-indigo-500"></i>
|
||
<span>FSRS (KI)</span>
|
||
</button>
|
||
<button id="algoLeitnerBtn" onclick="setSpacedRepetitionAlgo('leitner')" class="px-2.5 sm:px-3 py-1.5 rounded-lg text-slate-600 hover:text-slate-900 transition font-semibold flex items-center gap-1.5">
|
||
<i class="fa-solid fa-boxes-stacked"></i>
|
||
<span>5-Kasten</span>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Card Face Toggle -->
|
||
<div class="inline-flex p-1 bg-slate-100 rounded-xl border border-slate-200 text-xs sm:text-sm font-bold">
|
||
<button id="cardModeTermBtn" onclick="setCardFaceMode('term')" class="px-3 py-1.5 rounded-lg bg-white text-indigo-700 shadow-sm transition font-bold">
|
||
Begriff
|
||
</button>
|
||
<button id="cardModeExampleBtn" onclick="setCardFaceMode('example')" class="px-3 py-1.5 rounded-lg text-slate-600 hover:text-slate-900 transition font-semibold">
|
||
Beispiel
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Shuffle & Reset -->
|
||
<div class="flex items-center gap-1.5">
|
||
<button onclick="shuffleCards()" title="Karten mischen" class="p-2 sm:px-3 sm:py-2 rounded-xl border border-slate-200 hover:bg-slate-100 text-slate-600 text-xs sm:text-sm font-bold transition flex items-center gap-1">
|
||
<i class="fa-solid fa-shuffle"></i>
|
||
</button>
|
||
<button onclick="resetCardProgressPrompt()" title="Lernstand für Karten zurücksetzen" class="p-2 sm:px-3 sm:py-2 rounded-xl border border-slate-200 hover:bg-rose-50 hover:text-rose-600 text-slate-500 text-xs sm:text-sm font-bold transition flex items-center gap-1">
|
||
<i class="fa-solid fa-rotate-left"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Filter Pills -->
|
||
<div class="pt-2 border-t border-slate-100 flex flex-wrap items-center justify-between gap-2.5 text-xs sm:text-sm">
|
||
<!-- Selection & Category Selector -->
|
||
<div class="flex flex-wrap items-center gap-1.5">
|
||
<span class="font-bold text-slate-400 uppercase text-[11px] mr-1">Auswahl:</span>
|
||
<button onclick="setCardCategoryFilter('highlighted')" id="cardCatBtn-highlighted" class="card-cat-pill px-3 py-1.5 rounded-xl font-bold bg-indigo-50 text-indigo-700 border border-indigo-200">🌟 Kernstoff (17)</button>
|
||
<button onclick="setCardCategoryFilter('all')" id="cardCatBtn-all" class="card-cat-pill px-3 py-1.5 rounded-xl font-semibold bg-slate-100 text-slate-600 hover:bg-slate-200">Alle (23)</button>
|
||
<button onclick="setCardCategoryFilter('zusatz')" id="cardCatBtn-zusatz" class="card-cat-pill px-3 py-1.5 rounded-xl font-semibold bg-slate-100 text-slate-600 hover:bg-slate-200">🔍 Zusatzinfo (6)</button>
|
||
<span class="text-slate-300 mx-0.5">|</span>
|
||
<button onclick="setCardCategoryFilter('klang')" id="cardCatBtn-klang" class="card-cat-pill px-3 py-1.5 rounded-xl font-semibold bg-slate-100 text-slate-600 hover:bg-slate-200">Klang</button>
|
||
<button onclick="setCardCategoryFilter('syntax')" id="cardCatBtn-syntax" class="card-cat-pill px-3 py-1.5 rounded-xl font-semibold bg-slate-100 text-slate-600 hover:bg-slate-200">Syntax</button>
|
||
<button onclick="setCardCategoryFilter('wort')" id="cardCatBtn-wort" class="card-cat-pill px-3 py-1.5 rounded-xl font-semibold bg-slate-100 text-slate-600 hover:bg-slate-200">Wort / Tropen</button>
|
||
<button onclick="setCardCategoryFilter('gedanke')" id="cardCatBtn-gedanke" class="card-cat-pill px-3 py-1.5 rounded-xl font-semibold bg-slate-100 text-slate-600 hover:bg-slate-200">Gedanken</button>
|
||
</div>
|
||
|
||
<!-- Leitner Box Filter (Active in Leitner Mode) -->
|
||
<div id="leitnerFilterWrap" class="flex items-center gap-1.5 hidden">
|
||
<span class="font-bold text-slate-400 uppercase text-[11px] mr-1">Kasten:</span>
|
||
<select id="leitnerBoxFilter" onchange="filterByLeitnerBox(this.value)" class="bg-slate-100 border border-slate-200 text-slate-700 text-xs sm:text-sm rounded-xl px-3 py-1.5 font-bold focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
||
<option value="all">Alle Kästen (1-5)</option>
|
||
<option value="1">Kasten 1 (Neu / Unsicher)</option>
|
||
<option value="2">Kasten 2 (Im Aufbau)</option>
|
||
<option value="3">Kasten 3 (Vertieft)</option>
|
||
<option value="4">Kasten 4 (Fast sicher)</option>
|
||
<option value="5">Kasten 5 (Gemeistert)</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- FSRS Queue Filter (Active in FSRS Mode) -->
|
||
<div id="fsrsFilterWrap" class="flex items-center gap-1.5">
|
||
<span class="font-bold text-slate-400 uppercase text-[11px] mr-1">Intervall:</span>
|
||
<select id="fsrsQueueFilter" onchange="filterByFsrsQueue(this.value)" class="bg-indigo-50/70 border border-indigo-200 text-indigo-900 text-xs sm:text-sm rounded-xl px-3 py-1.5 font-extrabold focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
||
<option value="all">Alle Karten</option>
|
||
<option value="due" selected>📅 Jetzt fällig</option>
|
||
<option value="learning">📈 In Wiederholung</option>
|
||
<option value="new">🌱 Noch unberührt</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Progress bar & Algorithm Indicators -->
|
||
<div class="space-y-1.5">
|
||
<div class="flex flex-wrap justify-between items-center text-xs sm:text-sm text-slate-600 font-bold px-1 gap-2">
|
||
<span id="cardProgressLabel">Karte 1 von 17 (🌟 Kernstoff)</span>
|
||
|
||
<!-- Leitner Box Visual Indicators -->
|
||
<div id="leitnerMetricsContainer" class="flex items-center gap-2 hidden">
|
||
<span id="cardBoxBadge" class="px-2.5 py-0.5 rounded-lg bg-amber-50 text-amber-800 border border-amber-200 font-bold text-xs">Kasten 1</span>
|
||
<div id="leitnerBoxesVisual" class="flex gap-1">
|
||
<span class="w-2.5 h-2.5 rounded-full bg-indigo-600"></span>
|
||
<span class="w-2.5 h-2.5 rounded-full bg-slate-200"></span>
|
||
<span class="w-2.5 h-2.5 rounded-full bg-slate-200"></span>
|
||
<span class="w-2.5 h-2.5 rounded-full bg-slate-200"></span>
|
||
<span class="w-2.5 h-2.5 rounded-full bg-slate-200"></span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- FSRS DSR Live Metrics -->
|
||
<div id="fsrsMetricsContainer" class="flex flex-wrap items-center gap-1.5 text-[11px] sm:text-xs">
|
||
<span id="fsrsDueBadge" class="px-2.5 py-0.5 rounded-lg bg-rose-50 text-rose-700 border border-rose-200 font-extrabold flex items-center gap-1">
|
||
<i class="fa-solid fa-clock"></i>
|
||
<span>Fällig</span>
|
||
</span>
|
||
<span id="fsrsRetrievabilityBadge" title="Retrievability: Geschätzte Abrufwahrscheinlichkeit" class="px-2.5 py-0.5 rounded-lg bg-emerald-50 text-emerald-800 border border-emerald-200 font-extrabold flex items-center gap-1">
|
||
<i class="fa-solid fa-gauge-high text-emerald-600"></i>
|
||
<span>R: 90%</span>
|
||
</span>
|
||
<span id="fsrsStabilityBadge" title="Stability: Gedächtnisstabilität in Tagen (Intervall bis R=90%)" class="px-2.5 py-0.5 rounded-lg bg-blue-50 text-blue-800 border border-blue-200 font-bold flex items-center gap-1">
|
||
<i class="fa-solid fa-calendar-day text-blue-500"></i>
|
||
<span>S: 3.1 T</span>
|
||
</span>
|
||
<span id="fsrsDifficultyBadge" title="Difficulty: Berechnete Kartenschwierigkeit von 1 bis 10" class="px-2.5 py-0.5 rounded-lg bg-amber-50 text-amber-800 border border-amber-200 font-bold flex items-center gap-1">
|
||
<i class="fa-solid fa-weight-scale text-amber-500"></i>
|
||
<span>D: 5.3</span>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div class="w-full bg-slate-200 h-2.5 rounded-full overflow-hidden">
|
||
<div id="cardProgressBar" class="bg-indigo-600 h-full transition-all duration-300 w-0"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Flashcard 3D Stage -->
|
||
<div class="max-w-xl mx-auto py-2 relative">
|
||
<div class="perspective-1000 w-full h-[380px] sm:h-[410px] cursor-pointer touch-pan-y" id="flashcardContainer">
|
||
<div id="flashcardElement" class="relative w-full h-full transition-transform duration-500 transform-style-3d select-none">
|
||
|
||
<!-- CARD FRONT -->
|
||
<div class="absolute inset-0 w-full h-full backface-hidden bg-white rounded-3xl border-2 border-indigo-100 shadow-xl p-6 sm:p-8 flex flex-col justify-between items-center text-center select-none hover:border-indigo-300 transition-colors">
|
||
<div class="w-full flex justify-between items-center text-xs sm:text-sm font-bold text-slate-400 uppercase tracking-wider">
|
||
<span id="cardCounter">Karte 1 von 17</span>
|
||
<span id="cardBadgeFront" class="px-3 py-1 rounded-full bg-indigo-50 text-indigo-700 border border-indigo-100 font-bold lowercase">Klangfigur</span>
|
||
</div>
|
||
|
||
<!-- Main Front Content -->
|
||
<div class="my-auto px-2 w-full">
|
||
<div id="cardFrontHint" class="text-xs sm:text-sm font-extrabold uppercase tracking-wider text-indigo-600 mb-2">Rhetorisches Stilmittel</div>
|
||
<div id="cardFrontMain" class="text-3xl sm:text-4xl font-black text-slate-900 leading-tight tracking-tight">
|
||
Alliteration
|
||
</div>
|
||
<div id="cardFrontSub" class="text-sm sm:text-base text-slate-500 mt-4 font-medium italic">
|
||
Tippe oder drücke [Leertaste] zum Aufdecken
|
||
</div>
|
||
<div class="mt-3">
|
||
<button onclick="speakCurrentCard(event)" title="Laut vorlesen" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-slate-100 hover:bg-indigo-50 text-slate-600 hover:text-indigo-600 border border-slate-200 text-xs font-bold transition">
|
||
<i class="fa-solid fa-volume-high text-indigo-500"></i>
|
||
<span>Vorlesen</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="text-xs sm:text-sm text-slate-400 flex items-center gap-2 font-semibold">
|
||
<span class="hidden sm:inline-block px-1.5 py-0.5 bg-slate-100 border border-slate-200 rounded font-mono text-[10px]">Space</span>
|
||
<span><i class="fa-solid fa-arrow-rotate-right text-indigo-400"></i> Antippen zum Wenden</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- CARD BACK -->
|
||
<div class="absolute inset-0 w-full h-full backface-hidden rotate-y-180 bg-gradient-to-b from-indigo-950 via-slate-900 to-slate-950 text-white rounded-3xl border-2 border-indigo-500/30 shadow-2xl p-6 sm:p-8 flex flex-col justify-between text-left select-none">
|
||
<div class="flex justify-between items-center">
|
||
<span id="cardBackTerm" class="text-xl sm:text-2xl font-black text-indigo-300 flex items-center gap-2">
|
||
<i class="fa-regular fa-lightbulb text-amber-400"></i>
|
||
<span>Alliteration</span>
|
||
</span>
|
||
<span id="cardHighlightBadge" class="text-xs font-bold uppercase px-3 py-1 rounded-full bg-indigo-500/30 text-indigo-200 border border-indigo-400/40">
|
||
🌟 Kernstoff
|
||
</span>
|
||
</div>
|
||
|
||
<!-- Content Back -->
|
||
<div class="space-y-3.5 my-auto overflow-y-auto pr-1">
|
||
<div>
|
||
<span class="text-xs font-bold text-slate-400 uppercase tracking-wider block">Definition & Wirkung</span>
|
||
<p id="cardBackDefinition" class="text-base sm:text-lg text-slate-100 font-medium mt-1 leading-relaxed">
|
||
Mehrere Wörter beginnen mit demselben Anlaut.
|
||
</p>
|
||
</div>
|
||
|
||
<div class="bg-white/10 p-3.5 rounded-2xl border border-white/10 backdrop-blur-sm relative">
|
||
<div class="flex items-center justify-between">
|
||
<span class="text-xs font-bold text-indigo-300 uppercase tracking-wider block">Typische Phrasen & Zitate</span>
|
||
<button onclick="speakCurrentCard(event)" title="Beispiele vorlesen" class="px-2 py-1 rounded-lg bg-white/20 hover:bg-white/30 text-white text-xs font-bold transition flex items-center gap-1">
|
||
<i class="fa-solid fa-volume-high text-xs"></i>
|
||
<span>Vorlesen</span>
|
||
</button>
|
||
</div>
|
||
<p id="cardBackExamples" class="text-base sm:text-lg font-serif italic text-amber-200 mt-1">
|
||
„Fischers Fritz fischt frische Fische.“
|
||
</p>
|
||
</div>
|
||
|
||
<div id="cardBackTipContainer" class="bg-amber-950/40 p-3 rounded-2xl border border-amber-500/20">
|
||
<span class="text-xs font-bold text-amber-400 uppercase tracking-wider block flex items-center gap-1">
|
||
<i class="fa-solid fa-wand-magic-sparkles"></i> Merkhilfe & Tipp
|
||
</span>
|
||
<p id="cardBackTip" class="text-xs sm:text-sm text-amber-100 mt-0.5 leading-relaxed"></p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="text-xs sm:text-sm text-slate-400 flex justify-between items-center pt-2 border-t border-white/10">
|
||
<span id="cardBackCategory" class="font-semibold">Klangfigur</span>
|
||
<span class="text-slate-300 font-medium"><i class="fa-solid fa-repeat"></i> Nochmal tippen</span>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Action Buttons -->
|
||
<!-- Action Buttons -->
|
||
<div class="flex items-center justify-between gap-2 sm:gap-4 mt-6">
|
||
<button onclick="prevCard()" class="px-3.5 sm:px-4 py-3 sm:py-3.5 rounded-2xl bg-white border border-slate-200 hover:bg-slate-50 text-slate-700 font-bold text-sm sm:text-base shadow-sm transition flex items-center gap-1.5 flex-shrink-0">
|
||
<i class="fa-solid fa-chevron-left"></i>
|
||
<span class="hidden sm:inline">Zurück</span>
|
||
</button>
|
||
|
||
<!-- LEITNER ACTION BUTTONS (2 Options) -->
|
||
<div id="leitnerActionButtons" class="flex items-center gap-2.5 sm:gap-3 hidden">
|
||
<button onclick="markCardMastered(false)" class="px-4 sm:px-6 py-3 sm:py-3.5 rounded-2xl bg-rose-50 hover:bg-rose-100 text-rose-700 border border-rose-200 font-extrabold text-sm sm:text-base transition flex items-center gap-2 shadow-sm">
|
||
<span class="hidden md:inline-block px-1.5 py-0.5 bg-rose-200/80 rounded font-mono text-xs">1</span>
|
||
<i class="fa-solid fa-rotate-left"></i>
|
||
<span>Noch üben</span>
|
||
</button>
|
||
<button onclick="markCardMastered(true)" class="px-5 sm:px-7 py-3 sm:py-3.5 rounded-2xl bg-emerald-600 hover:bg-emerald-700 text-white font-extrabold text-sm sm:text-base shadow-lg shadow-emerald-600/25 transition flex items-center gap-2">
|
||
<span class="hidden md:inline-block px-1.5 py-0.5 bg-emerald-800 rounded font-mono text-xs">2</span>
|
||
<i class="fa-solid fa-check"></i>
|
||
<span>Gekonnt!</span>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- FSRS ACTION BUTTONS (4 Ratings with Predicted Next Interval Badges) -->
|
||
<div id="fsrsActionButtons" class="flex flex-wrap items-center justify-center gap-1.5 sm:gap-2.5">
|
||
<!-- 1: Nochmal / Again -->
|
||
<button onclick="rateFsrsCard(1)" class="px-2.5 sm:px-4 py-2 sm:py-2.5 rounded-2xl bg-rose-50 hover:bg-rose-100 text-rose-700 border border-rose-200 font-extrabold text-xs sm:text-sm transition flex flex-col items-center shadow-sm hover:scale-[1.02] active:scale-95">
|
||
<div class="flex items-center gap-1">
|
||
<span class="hidden md:inline-block px-1 py-0.2 bg-rose-200/80 rounded font-mono text-[10px]">1</span>
|
||
<i class="fa-solid fa-rotate-left"></i>
|
||
<span>Nochmal</span>
|
||
</div>
|
||
<span id="fsrsIntervalAgain" class="text-[10px] text-rose-600 font-semibold mt-0.5">< 10 Min</span>
|
||
</button>
|
||
|
||
<!-- 2: Schwer / Hard -->
|
||
<button onclick="rateFsrsCard(2)" class="px-2.5 sm:px-4 py-2 sm:py-2.5 rounded-2xl bg-amber-50 hover:bg-amber-100 text-amber-800 border border-amber-200 font-extrabold text-xs sm:text-sm transition flex flex-col items-center shadow-sm hover:scale-[1.02] active:scale-95">
|
||
<div class="flex items-center gap-1">
|
||
<span class="hidden md:inline-block px-1 py-0.2 bg-amber-200/80 rounded font-mono text-[10px]">2</span>
|
||
<i class="fa-solid fa-triangle-exclamation"></i>
|
||
<span>Schwer</span>
|
||
</div>
|
||
<span id="fsrsIntervalHard" class="text-[10px] text-amber-700 font-semibold mt-0.5">+1 Tag</span>
|
||
</button>
|
||
|
||
<!-- 3: Gut / Good -->
|
||
<button onclick="rateFsrsCard(3)" class="px-2.5 sm:px-4 py-2 sm:py-2.5 rounded-2xl bg-emerald-50 hover:bg-emerald-100 text-emerald-800 border border-emerald-300 font-extrabold text-xs sm:text-sm transition flex flex-col items-center shadow-sm hover:scale-[1.02] active:scale-95">
|
||
<div class="flex items-center gap-1">
|
||
<span class="hidden md:inline-block px-1 py-0.2 bg-emerald-200/80 rounded font-mono text-[10px]">3</span>
|
||
<i class="fa-solid fa-check"></i>
|
||
<span>Gut</span>
|
||
</div>
|
||
<span id="fsrsIntervalGood" class="text-[10px] text-emerald-700 font-semibold mt-0.5">+3 Tage</span>
|
||
</button>
|
||
|
||
<!-- 4: Einfach / Easy -->
|
||
<button onclick="rateFsrsCard(4)" class="px-2.5 sm:px-4 py-2 sm:py-2.5 rounded-2xl bg-indigo-50 hover:bg-indigo-100 text-indigo-700 border border-indigo-200 font-extrabold text-xs sm:text-sm transition flex flex-col items-center shadow-sm hover:scale-[1.02] active:scale-95">
|
||
<div class="flex items-center gap-1">
|
||
<span class="hidden md:inline-block px-1 py-0.2 bg-indigo-200/80 rounded font-mono text-[10px]">4</span>
|
||
<i class="fa-solid fa-wand-magic-sparkles"></i>
|
||
<span>Einfach</span>
|
||
</div>
|
||
<span id="fsrsIntervalEasy" class="text-[10px] text-indigo-600 font-semibold mt-0.5">+15 Tage</span>
|
||
</button>
|
||
</div>
|
||
|
||
<button onclick="nextCard()" class="px-3.5 sm:px-4 py-3 sm:py-3.5 rounded-2xl bg-white border border-slate-200 hover:bg-slate-50 text-slate-700 font-bold text-sm sm:text-base shadow-sm transition flex items-center gap-1.5 flex-shrink-0">
|
||
<span class="hidden sm:inline">Weiter</span>
|
||
<i class="fa-solid fa-chevron-right"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ========================================== -->
|
||
<!-- VIEW 2: QUIZ MODUS & SCHWACHSTELLENTRAINER -->
|
||
<!-- ========================================== -->
|
||
<section id="view-quiz" class="hidden space-y-4 sm:space-y-6">
|
||
<!-- Quiz Header / Stat Bar -->
|
||
<div id="quizActiveHeader" class="bg-white p-4 sm:p-5 rounded-2xl border border-slate-200 shadow-sm flex flex-wrap items-center justify-between gap-3 sm:gap-4">
|
||
<div class="flex items-center gap-3">
|
||
<div class="w-11 h-11 rounded-2xl bg-violet-100 text-violet-700 flex items-center justify-center font-bold text-xl shadow-sm">
|
||
<i class="fa-solid fa-brain"></i>
|
||
</div>
|
||
<div>
|
||
<div class="text-xs sm:text-sm font-bold text-slate-500 uppercase tracking-wider flex items-center gap-2">
|
||
<span id="quizModeName">🌟 Kernstoff-Challenge (17 Stilmittel)</span>
|
||
</div>
|
||
<div id="quizQuestionNumber" class="text-lg sm:text-2xl font-black text-slate-900">Frage 1 von 10</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Quiz Mode & Type Selectors -->
|
||
<div class="flex flex-col sm:flex-row flex-wrap items-start sm:items-center gap-2.5 w-full lg:w-auto justify-between lg:justify-end">
|
||
<!-- Modes: Kernstoff | Alle | Zusatz | Schwachstellen | Prüfung -->
|
||
<div class="flex flex-wrap items-center gap-1.5 p-1 bg-slate-100 rounded-xl border border-slate-200 text-xs sm:text-sm font-bold">
|
||
<button id="quizModeKernBtn" onclick="startQuiz('highlighted')" class="px-2.5 sm:px-3 py-1.5 rounded-lg bg-white text-indigo-700 shadow-sm font-bold transition">🌟 Kernstoff (17)</button>
|
||
<button id="quizModeAllBtn" onclick="startQuiz('all')" class="px-2.5 sm:px-3 py-1.5 rounded-lg text-slate-600 hover:text-slate-900 transition font-semibold">Alle (23)</button>
|
||
<button id="quizModeZusatzBtn" onclick="startQuiz('zusatz')" class="px-2.5 sm:px-3 py-1.5 rounded-lg text-slate-600 hover:text-slate-900 transition font-semibold">🔍 Zusatz (6)</button>
|
||
<button id="quizModeWeakBtn" onclick="startWeakSpotsQuiz()" class="px-2.5 sm:px-3 py-1.5 rounded-lg text-rose-700 hover:bg-rose-50 transition font-bold flex items-center gap-1">
|
||
<i class="fa-solid fa-triangle-exclamation text-xs"></i>
|
||
<span>Schwachstellen (<span id="quizWeakBtnCount">0</span>)</span>
|
||
</button>
|
||
<button id="quizModeExamBtn" onclick="startQuiz('exam')" class="px-2.5 sm:px-3 py-1.5 rounded-lg bg-gradient-to-r from-amber-500 to-orange-500 text-white shadow-sm font-extrabold transition flex items-center gap-1">
|
||
<i class="fa-solid fa-stopwatch text-xs"></i>
|
||
<span>Prüfungssimulation</span>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Input Format Toggle: Multiple Choice vs Freitext -->
|
||
<div class="inline-flex p-1 bg-slate-100 rounded-xl border border-slate-200 text-xs font-bold">
|
||
<button id="quizInputChoiceBtn" onclick="setQuizInputType('choice')" class="px-2.5 py-1.5 rounded-lg bg-white text-indigo-700 shadow-sm font-bold transition flex items-center gap-1">
|
||
<i class="fa-solid fa-list-check"></i>
|
||
<span>Multiple-Choice</span>
|
||
</button>
|
||
<button id="quizInputTextBtn" onclick="setQuizInputType('text')" class="px-2.5 py-1.5 rounded-lg text-slate-600 hover:text-slate-900 transition font-semibold flex items-center gap-1">
|
||
<i class="fa-solid fa-keyboard"></i>
|
||
<span>Freitext-Eingabe</span>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Streak & Score counters (hidden during Exam) -->
|
||
<div id="quizScoreStatsContainer" class="flex items-center gap-2 text-xs sm:text-sm font-bold">
|
||
<div class="flex items-center gap-1.5 text-amber-700 bg-amber-50 px-3 py-1.5 rounded-xl border border-amber-200/70">
|
||
<i class="fa-solid fa-fire text-amber-500"></i>
|
||
<span>Streak: <span id="quizStreak">0</span></span>
|
||
</div>
|
||
<div class="flex items-center gap-1.5 text-indigo-700 bg-indigo-50 px-3 py-1.5 rounded-xl border border-indigo-200/70">
|
||
<i class="fa-solid fa-trophy text-indigo-600"></i>
|
||
<span>Punkte: <span id="quizScore">0</span></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- EXAM TIMER & STATUS BAR (Visible only during Exam Simulation) -->
|
||
<div id="examTimerBar" class="hidden bg-gradient-to-r from-slate-900 to-indigo-950 text-white rounded-2xl p-4 sm:p-5 shadow-lg max-w-2xl mx-auto flex flex-col sm:flex-row items-center justify-between gap-3">
|
||
<div class="flex items-center gap-3">
|
||
<div class="w-10 h-10 rounded-xl bg-amber-500 text-slate-950 flex items-center justify-center font-black text-xl shadow">
|
||
<i class="fa-solid fa-stopwatch"></i>
|
||
</div>
|
||
<div>
|
||
<div class="text-[11px] font-bold text-indigo-200 uppercase tracking-wider">Klausur-Simulation (15 Fragen)</div>
|
||
<div class="text-sm font-semibold text-slate-300">Kein Sofort-Feedback • Notenskala 1 bis 6</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex items-center gap-4 w-full sm:w-auto justify-between sm:justify-end">
|
||
<div class="text-right">
|
||
<div class="text-[10px] uppercase font-bold text-slate-400">Verbleibende Zeit</div>
|
||
<div id="examTimerDisplay" class="font-mono text-2xl font-black text-amber-400 tracking-wider">10:00</div>
|
||
</div>
|
||
<button onclick="confirmSubmitExamEarly()" class="px-3.5 py-2 rounded-xl bg-rose-600 hover:bg-rose-700 text-white font-bold text-xs sm:text-sm shadow transition flex items-center gap-1.5 whitespace-nowrap">
|
||
<i class="fa-solid fa-paper-plane"></i>
|
||
<span>Abgeben</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Active Question Container -->
|
||
<div id="quizPlayArea" class="bg-white rounded-3xl border border-slate-200 shadow-xl p-5 sm:p-8 max-w-2xl mx-auto space-y-5 sm:space-y-6">
|
||
<div>
|
||
<div class="flex items-center justify-between">
|
||
<span id="quizCategoryTag" class="px-3.5 py-1.5 rounded-full text-xs sm:text-sm font-extrabold uppercase tracking-wider bg-violet-100 text-violet-800 border border-violet-200">
|
||
Beispiel-Zuordnung
|
||
</span>
|
||
<span class="text-xs text-slate-400 hidden sm:inline">Tastatur: [1], [2], [3], [4]</span>
|
||
</div>
|
||
<h3 id="quizQuestionPrompt" class="text-xl sm:text-2xl font-black text-slate-900 mt-3 leading-snug">
|
||
Welches rhetorische Stilmittel liegt in diesem Zitat vor?
|
||
</h3>
|
||
<div class="mt-4 relative group">
|
||
<div id="quizPhraseBox" class="p-5 sm:p-6 pr-14 bg-slate-50 border-l-4 border-indigo-600 rounded-2xl font-serif text-xl sm:text-2xl text-slate-900 italic shadow-inner leading-relaxed">
|
||
„Die Kunst ist lang, und kurz ist unser Leben.“
|
||
</div>
|
||
<!-- Audio Read Aloud Button -->
|
||
<button onclick="speakCurrentQuizPhrase(event)" title="Beispiel / Zitat laut vorlesen (Web Speech)" class="absolute right-3 top-3 sm:top-4 w-10 h-10 rounded-xl bg-white hover:bg-indigo-50 text-indigo-600 border border-slate-200 hover:border-indigo-300 shadow-sm flex items-center justify-center transition" aria-label="Audio Vorlesen">
|
||
<i class="fa-solid fa-volume-high text-base"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 4 Multiple Choice Answers -->
|
||
<div id="quizOptionsContainer" class="grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-3.5 pt-2">
|
||
<!-- Options dynamically generated here -->
|
||
</div>
|
||
|
||
<!-- Freitext Input Container (Active when quizInputType === 'text') -->
|
||
<div id="quizFreitextContainer" class="hidden space-y-3 pt-2">
|
||
<div class="relative">
|
||
<input type="text" id="quizFreitextInput" onkeydown="handleFreitextKeydown(event)" placeholder="Stilmittel eintippen (z. B. Alliteration, Chiasmus)..." class="w-full px-5 py-4 pr-32 rounded-2xl border-2 border-slate-300 focus:border-indigo-600 focus:outline-none text-base sm:text-lg font-bold text-slate-900 placeholder:text-slate-400 shadow-inner">
|
||
<button onclick="submitFreitextAnswer()" class="absolute right-2.5 top-2.5 bottom-2.5 px-6 rounded-xl bg-indigo-600 hover:bg-indigo-700 text-white font-extrabold text-sm sm:text-base shadow transition flex items-center gap-1.5">
|
||
<span>Prüfen</span>
|
||
<i class="fa-solid fa-arrow-right text-xs"></i>
|
||
</button>
|
||
</div>
|
||
<p class="text-xs text-slate-500 font-medium flex items-center gap-1">
|
||
<i class="fa-solid fa-wand-magic-sparkles text-indigo-500"></i>
|
||
<span>Aktives Abrufen trainiert das Langzeitgedächtnis. Tippfehler werden automatisch toleriert!</span>
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Inline Explanation Feedback -->
|
||
<div id="quizFeedbackBox" class="hidden rounded-2xl p-4 sm:p-6 border transition-all">
|
||
<div class="flex items-start gap-3.5">
|
||
<div id="quizFeedbackIcon" class="text-3xl mt-0.5"></div>
|
||
<div class="flex-1">
|
||
<h4 id="quizFeedbackTitle" class="font-extrabold text-base sm:text-lg">Richtig gelöst!</h4>
|
||
<p id="quizFeedbackText" class="text-sm sm:text-base mt-1.5 leading-relaxed"></p>
|
||
</div>
|
||
</div>
|
||
<div class="mt-5 flex justify-end">
|
||
<button id="quizNextBtn" onclick="nextQuizQuestion()" class="px-7 py-3 rounded-2xl bg-slate-900 hover:bg-slate-800 text-white font-extrabold text-base shadow-lg transition flex items-center gap-2.5">
|
||
<span>Nächste Frage</span>
|
||
<span class="hidden sm:inline-block px-1.5 py-0.5 bg-slate-700 rounded font-mono text-xs">Enter</span>
|
||
<i class="fa-solid fa-arrow-right"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- EXAM NAVIGATION BAR (Only visible during Exam Simulation) -->
|
||
<div id="examNavBar" class="hidden pt-3 border-t border-slate-200 flex items-center justify-between gap-3">
|
||
<button id="examPrevBtn" onclick="prevExamQuestion()" class="px-4 sm:px-5 py-2.5 rounded-xl border border-slate-300 hover:bg-slate-50 text-slate-700 font-bold text-xs sm:text-sm transition flex items-center gap-1.5">
|
||
<i class="fa-solid fa-chevron-left"></i>
|
||
<span>Zurück</span>
|
||
</button>
|
||
|
||
<div id="examQuestionTracker" class="text-xs sm:text-sm font-bold text-slate-500">
|
||
Frage 1 von 15
|
||
</div>
|
||
|
||
<button id="examNextBtn" onclick="nextExamQuestion()" class="px-5 sm:px-6 py-2.5 rounded-xl bg-slate-900 hover:bg-slate-800 text-white font-bold text-xs sm:text-sm shadow transition flex items-center gap-1.5">
|
||
<span>Weiter</span>
|
||
<i class="fa-solid fa-chevron-right"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
<!-- EXAM RESULT SCREEN (Klausur-Zeugnis mit Notenschlüssel) -->
|
||
<div id="examResultScreen" class="hidden bg-white rounded-3xl border border-slate-200 shadow-xl p-6 sm:p-8 max-w-2xl mx-auto space-y-6">
|
||
<div class="text-center space-y-2">
|
||
<div id="examGradeBadgeContainer" class="inline-flex items-center gap-2 px-5 py-2 rounded-2xl border-2 font-black text-lg sm:text-2xl shadow-sm">
|
||
<i id="examGradeIcon" class="fa-solid fa-crown text-amber-500"></i>
|
||
<span id="examGradeTitle">Note: Sehr gut (1)</span>
|
||
</div>
|
||
<h3 class="text-2xl sm:text-3xl font-black text-slate-900 mt-2">Prüfungsauswertung</h3>
|
||
<p id="examPedagogicFeedback" class="text-sm sm:text-base text-slate-600 font-medium max-w-lg mx-auto">
|
||
Hervorragende Leistung! Du beherrschst die rhetorischen Stilmittel souverän.
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Exam Performance Stats -->
|
||
<div class="bg-slate-50 p-4 sm:p-5 rounded-2xl border border-slate-200 grid grid-cols-3 gap-2.5 text-center">
|
||
<div>
|
||
<div class="text-[11px] font-bold text-slate-400 uppercase">Richtig gelöst</div>
|
||
<div id="examScoreDisplay" class="text-xl sm:text-3xl font-black text-indigo-600 mt-1">14 / 15</div>
|
||
</div>
|
||
<div>
|
||
<div class="text-[11px] font-bold text-slate-400 uppercase">Quote</div>
|
||
<div id="examPercentDisplay" class="text-xl sm:text-3xl font-black text-emerald-600 mt-1">93%</div>
|
||
</div>
|
||
<div>
|
||
<div class="text-[11px] font-bold text-slate-400 uppercase">Zeit</div>
|
||
<div id="examTimeDisplay" class="text-xl sm:text-3xl font-black text-amber-600 mt-1">04:32 Min</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Itemized Exam Review (Alle 15 Fragen zur Nachbesprechung) -->
|
||
<div class="space-y-3 text-left">
|
||
<div class="flex items-center justify-between border-b border-slate-200 pb-2">
|
||
<h4 class="text-xs sm:text-sm font-extrabold text-slate-700 uppercase tracking-wider flex items-center gap-2">
|
||
<i class="fa-solid fa-list-check text-indigo-600"></i>
|
||
<span>Detaillierte Fragen-Nachbesprechung (15 Aufgaben)</span>
|
||
</h4>
|
||
<span class="text-xs text-slate-400">Klausur-Korrektur</span>
|
||
</div>
|
||
<div id="examReviewList" class="space-y-3 max-h-96 overflow-y-auto pr-1 text-xs sm:text-sm">
|
||
<!-- Populated dynamically -->
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Action Buttons -->
|
||
<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">
|
||
<i class="fa-solid fa-rotate-right"></i>
|
||
<span>Prüfung wiederholen</span>
|
||
</button>
|
||
<button id="examRepeatMistakesBtn" onclick="startWeakSpotsQuiz()" class="hidden px-6 py-3.5 rounded-2xl bg-rose-600 hover:bg-rose-700 text-white font-extrabold text-sm sm:text-base shadow-lg shadow-rose-600/20 transition flex items-center justify-center gap-2">
|
||
<i class="fa-solid fa-triangle-exclamation"></i>
|
||
<span>Fehler trainieren</span>
|
||
</button>
|
||
<button onclick="startQuiz('highlighted')" class="px-6 py-3.5 rounded-2xl bg-slate-100 hover:bg-slate-200 text-slate-700 font-bold text-sm sm:text-base transition">
|
||
Übungs-Quiz
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Quiz Results Screen -->
|
||
<div id="quizResultScreen" class="hidden bg-white rounded-3xl border border-slate-200 shadow-xl p-6 sm:p-8 max-w-xl mx-auto text-center space-y-6">
|
||
<div id="quizResultTrophy" class="w-20 h-20 sm:w-24 sm:h-24 mx-auto rounded-3xl bg-amber-100 text-amber-600 flex items-center justify-center text-4xl sm:text-5xl shadow-inner">
|
||
<i class="fa-solid fa-crown"></i>
|
||
</div>
|
||
<div>
|
||
<h3 class="text-2xl sm:text-3xl font-black text-slate-900">Quiz beendet!</h3>
|
||
<p id="quizResultMessage" class="text-sm sm:text-base text-slate-500 mt-1 font-medium">Hervorragende Leistung bei der Textanalyse!</p>
|
||
</div>
|
||
|
||
<div class="bg-slate-50 p-5 rounded-2xl border border-slate-200 grid grid-cols-3 gap-3 text-center">
|
||
<div>
|
||
<div class="text-xs font-bold text-slate-400 uppercase">Ergebnis</div>
|
||
<div id="resultScore" class="text-2xl sm:text-3xl font-black text-indigo-600 mt-1">8 / 10</div>
|
||
</div>
|
||
<div>
|
||
<div class="text-xs font-bold text-slate-400 uppercase">Quote</div>
|
||
<div id="resultPercent" class="text-2xl sm:text-3xl font-black text-emerald-600 mt-1">80%</div>
|
||
</div>
|
||
<div>
|
||
<div class="text-xs font-bold text-slate-400 uppercase">Max Streak</div>
|
||
<div id="resultMaxStreak" class="text-2xl sm:text-3xl font-black text-amber-600 mt-1">5</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Mistake Review Section -->
|
||
<div id="quizMistakesContainer" class="hidden text-left space-y-2">
|
||
<h4 class="text-xs sm:text-sm font-bold text-slate-400 uppercase tracking-wider">Zu wiederholen:</h4>
|
||
<div id="quizMistakesList" class="space-y-2 max-h-48 overflow-y-auto pr-1"></div>
|
||
</div>
|
||
|
||
<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">
|
||
<i class="fa-solid fa-rotate-right"></i>
|
||
<span>Nochmal spielen</span>
|
||
</button>
|
||
<button id="repeatMistakesBtn" onclick="startWeakSpotsQuiz()" class="hidden px-6 py-3.5 rounded-2xl bg-rose-600 hover:bg-rose-700 text-white font-extrabold text-sm sm:text-base shadow-lg shadow-rose-600/20 transition flex items-center justify-center gap-2">
|
||
<i class="fa-solid fa-triangle-exclamation"></i>
|
||
<span>Fehler wiederholen</span>
|
||
</button>
|
||
<button onclick="switchTab('cards')" class="px-6 py-3.5 rounded-2xl bg-slate-100 hover:bg-slate-200 text-slate-700 font-bold text-sm sm:text-base transition">
|
||
Karteikarten
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ========================================== -->
|
||
<!-- VIEW 3: PHRASEN-MATCHER (Mini-Game) -->
|
||
<!-- ========================================== -->
|
||
<section id="view-matcher" class="hidden space-y-4 sm:space-y-6">
|
||
<div class="bg-white p-4 sm:p-5 rounded-2xl border border-slate-200 shadow-sm flex flex-col lg:flex-row justify-between items-start lg:items-center gap-3 sm:gap-4">
|
||
<div>
|
||
<h2 class="text-base sm:text-xl font-extrabold text-slate-900 flex items-center gap-2">
|
||
<i class="fa-solid fa-puzzle-piece text-indigo-600"></i>
|
||
<span>Phrasen-Matcher (Direkt-Zuordnung)</span>
|
||
</h2>
|
||
<p class="text-xs sm:text-sm text-slate-500 mt-0.5">
|
||
Wähle ein Zitat und tippe auf das passende Stilmittel. <strong>Achtung: Nur 1 Versuch pro Paar – falsch ist falsch!</strong>
|
||
</p>
|
||
</div>
|
||
|
||
<div class="flex flex-wrap items-center gap-2.5 w-full lg:w-auto justify-between lg:justify-end">
|
||
<!-- Selection Filter: Kernstoff (bevorzugt) | Alle | Zusatz -->
|
||
<div class="inline-flex p-1 bg-slate-100 rounded-xl border border-slate-200 text-xs sm:text-sm font-bold">
|
||
<button id="matcherFilterKernBtn" onclick="setMatcherFilter('highlighted')" class="px-2.5 sm:px-3 py-1.5 rounded-lg bg-white text-indigo-700 shadow-sm font-bold transition">🌟 Kernstoff (17)</button>
|
||
<button id="matcherFilterAllBtn" onclick="setMatcherFilter('all')" class="px-2.5 sm:px-3 py-1.5 rounded-lg text-slate-600 hover:text-slate-900 transition font-semibold">Alle (23)</button>
|
||
<button id="matcherFilterZusatzBtn" onclick="setMatcherFilter('zusatz')" class="px-2.5 sm:px-3 py-1.5 rounded-lg text-slate-600 hover:text-slate-900 transition font-semibold">🔍 Zusatz (6)</button>
|
||
</div>
|
||
|
||
<!-- Score & Mistakes Badges -->
|
||
<div class="flex items-center gap-2 text-xs sm:text-sm font-bold">
|
||
<div class="px-3 py-1.5 bg-emerald-50 text-emerald-800 border border-emerald-200 rounded-xl flex items-center gap-1.5">
|
||
<i class="fa-solid fa-check text-emerald-600"></i>
|
||
<span>Richtig: <span id="matchScore">0</span> / 5</span>
|
||
</div>
|
||
<div class="px-3 py-1.5 bg-rose-50 text-rose-800 border border-rose-200 rounded-xl flex items-center gap-1.5">
|
||
<i class="fa-solid fa-xmark text-rose-600"></i>
|
||
<span>Fehler: <span id="matchFailScore">0</span></span>
|
||
</div>
|
||
<button onclick="initMatcherGame()" title="Neu mischen" class="p-2 sm:px-3 sm:py-2 bg-slate-100 hover:bg-slate-200 text-slate-700 rounded-xl font-bold transition flex items-center gap-1">
|
||
<i class="fa-solid fa-arrows-rotate"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Matcher Board -->
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 sm:gap-6 max-w-4xl mx-auto">
|
||
<!-- Left: Phrases -->
|
||
<div class="space-y-3">
|
||
<h3 class="text-xs sm:text-sm font-extrabold text-slate-400 uppercase tracking-wider px-1">Phrasen & Zitate</h3>
|
||
<div id="matcherPhrasesContainer" class="space-y-3">
|
||
<!-- Dynamically populated -->
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Right: Terms -->
|
||
<div class="space-y-3">
|
||
<h3 class="text-xs sm:text-sm font-extrabold text-slate-400 uppercase tracking-wider px-1">Rhetorische Stilmittel</h3>
|
||
<div id="matcherTermsContainer" class="space-y-3">
|
||
<!-- Dynamically populated -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Matcher Result Screen (Fehler & Richtig) -->
|
||
<div id="matcherResultBanner" class="hidden text-white p-6 sm:p-8 rounded-3xl text-center space-y-4 shadow-xl max-w-md mx-auto transition-all">
|
||
<div id="matcherResultIcon" class="text-4xl"><i class="fa-solid fa-circle-check"></i></div>
|
||
<div>
|
||
<h4 id="matcherResultTitle" class="text-2xl font-black">Perfekt zugeordnet!</h4>
|
||
<p id="matcherResultSubtitle" class="text-sm sm:text-base text-white/90 mt-1">Du hast alle 5 Paare fehlerfrei zusammengeführt.</p>
|
||
</div>
|
||
<div class="flex flex-col sm:flex-row gap-2.5 justify-center pt-2">
|
||
<button onclick="initMatcherGame()" class="px-6 py-3 rounded-2xl bg-white text-slate-900 font-extrabold text-sm sm:text-base shadow hover:bg-slate-100 transition">
|
||
Nächste Runde
|
||
</button>
|
||
<button id="matcherWeakspotsBtn" onclick="switchTab('quiz'); startWeakSpotsQuiz();" class="hidden px-5 py-3 rounded-2xl bg-rose-600 hover:bg-rose-700 text-white font-extrabold text-sm transition flex items-center justify-center gap-1.5">
|
||
<i class="fa-solid fa-triangle-exclamation"></i>
|
||
<span>Fehler im Quiz üben</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ========================================== -->
|
||
<!-- VIEW 4: GLOSSAR & SUCHE MIT KATEGORIEN -->
|
||
<!-- ========================================== -->
|
||
<section id="view-glossary" class="hidden space-y-4 sm:space-y-5">
|
||
<!-- Search & Filters -->
|
||
<div class="bg-white p-4 sm:p-5 rounded-2xl border border-slate-200 shadow-sm space-y-3.5">
|
||
<div class="flex flex-col sm:flex-row gap-3">
|
||
<div class="relative flex-1">
|
||
<i class="fa-solid fa-magnifying-glass absolute left-4 top-3.5 text-slate-400 text-base"></i>
|
||
<input type="text" id="glossarySearchInput" oninput="renderGlossary()" placeholder="Suche nach Begriff, Definition oder Beispiel (z. B. 'Chiasmus', 'Fug und Recht', 'Faust')... [/]" class="w-full pl-11 pr-4 py-3 rounded-2xl border border-slate-300 focus:outline-none focus:ring-2 focus:ring-indigo-500 text-sm sm:text-base">
|
||
</div>
|
||
|
||
<button onclick="printCheatsheet()" class="px-4 py-3 rounded-2xl text-xs sm:text-sm font-bold bg-slate-900 text-white hover:bg-slate-800 transition flex items-center justify-center gap-2 whitespace-nowrap shadow-sm">
|
||
<i class="fa-solid fa-print"></i>
|
||
<span>Als PDF / Druck</span>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Category Filters -->
|
||
<div class="flex flex-wrap items-center gap-1.5 pt-2 border-t border-slate-100 text-xs sm:text-sm">
|
||
<button id="filterAllBtn" onclick="setGlossaryFilter('all')" class="px-3 py-1.5 rounded-xl font-bold bg-indigo-50 text-indigo-700 border border-indigo-200">
|
||
Alle (<span id="countAllBadge">23</span>)
|
||
</button>
|
||
<button id="filterKernBtn" onclick="setGlossaryFilter('highlight')" class="px-3 py-1.5 rounded-xl font-semibold bg-slate-100 text-slate-600 hover:bg-slate-200 border border-slate-200">
|
||
🌟 Kernstoff (17)
|
||
</button>
|
||
<button id="filterZusatzBtn" onclick="setGlossaryFilter('zusatz')" class="px-3 py-1.5 rounded-xl font-semibold bg-slate-100 text-slate-600 hover:bg-slate-200 border border-slate-200">
|
||
🔍 Zusatzinfo (6)
|
||
</button>
|
||
<button id="filterKlangBtn" onclick="setGlossaryFilter('klang')" class="px-3 py-1.5 rounded-xl font-semibold bg-slate-100 text-slate-600 hover:bg-slate-200 border border-slate-200">
|
||
Klangfiguren
|
||
</button>
|
||
<button id="filterSyntaxBtn" onclick="setGlossaryFilter('syntax')" class="px-3 py-1.5 rounded-xl font-semibold bg-slate-100 text-slate-600 hover:bg-slate-200 border border-slate-200">
|
||
Satzfiguren / Syntax
|
||
</button>
|
||
<button id="filterWortBtn" onclick="setGlossaryFilter('wort')" class="px-3 py-1.5 rounded-xl font-semibold bg-slate-100 text-slate-600 hover:bg-slate-200 border border-slate-200">
|
||
Wortfiguren & Tropen
|
||
</button>
|
||
<button id="filterGedankeBtn" onclick="setGlossaryFilter('gedanke')" class="px-3 py-1.5 rounded-xl font-semibold bg-slate-100 text-slate-600 hover:bg-slate-200 border border-slate-200">
|
||
Gedankenfiguren
|
||
</button>
|
||
</div>
|
||
|
||
<div class="flex items-center justify-between text-xs sm:text-sm text-slate-500 px-1 pt-1">
|
||
<span id="glossaryResultCount" class="font-bold">23 Stilmittel gelistet</span>
|
||
<span class="hidden sm:inline">Tippe auf ein Stilmittel für Details, Merkhilfen und Klausur-Tipps</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Glossary Card Grid -->
|
||
<div id="glossaryGrid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3.5 sm:gap-4">
|
||
<!-- Rendered by JS -->
|
||
</div>
|
||
</section>
|
||
|
||
</main>
|
||
|
||
<!-- ========================================== -->
|
||
<!-- PRINT-READY CHEATSHEET SECTION (@media print) -->
|
||
<!-- ========================================== -->
|
||
<section id="printCheatsheetSection" class="hidden print:block p-6">
|
||
<div class="border-b-2 border-slate-800 pb-3 mb-4 flex justify-between items-end">
|
||
<div>
|
||
<h1 class="text-2xl font-black uppercase tracking-tight">RhetorikMaster - Klausur-Spickzettel</h1>
|
||
<p class="text-sm text-slate-600 mt-1">Rhetorische Stilmittel der Textanalyse • Definitionen, Merkhilfen & typische Zitate</p>
|
||
</div>
|
||
<div class="text-xs text-slate-500 font-mono text-right">
|
||
rhetorikmaster.pansi.eu
|
||
</div>
|
||
</div>
|
||
|
||
<table class="w-full text-xs text-left border border-slate-300">
|
||
<thead>
|
||
<tr class="bg-slate-100 text-slate-900 uppercase font-bold text-[10px] tracking-wider">
|
||
<th class="p-2 w-1/4">Stilmittel & Kategorie</th>
|
||
<th class="p-2 w-1/3">Definition & Wirkung</th>
|
||
<th class="p-2 w-1/4">Typisches Beispiel / Zitat</th>
|
||
<th class="p-2">Signal / Merkhilfe</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="printTableBody">
|
||
<!-- Dynamically rendered -->
|
||
</tbody>
|
||
</table>
|
||
</section>
|
||
|
||
<!-- DETAIL MODAL -->
|
||
<div id="detailModal" 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-lg 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">
|
||
<button onclick="closeDetailModal()" 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="space-y-4">
|
||
<div class="flex items-center gap-2">
|
||
<span id="modalHighlightBadge" class="text-xs font-bold px-2.5 py-0.5 rounded-full bg-indigo-50 text-indigo-700 border border-indigo-200 uppercase tracking-wider">
|
||
Stilmittel
|
||
</span>
|
||
<span id="modalCategoryBadge" class="text-xs font-bold px-2.5 py-0.5 rounded-full bg-slate-100 text-slate-700">
|
||
Satzfigur
|
||
</span>
|
||
</div>
|
||
|
||
<div>
|
||
<h3 id="modalTerm" class="text-2xl sm:text-3xl font-black text-slate-900">Chiasmus</h3>
|
||
<p id="modalCategory" class="text-xs sm:text-sm text-indigo-600 font-bold uppercase tracking-wider mt-0.5">Satzbaustilmittel / Überkreuzstellung</p>
|
||
</div>
|
||
|
||
<div class="space-y-3 pt-2">
|
||
<div class="bg-slate-50 p-4 sm:p-5 rounded-2xl border border-slate-100">
|
||
<div class="text-xs font-bold text-slate-400 uppercase tracking-wider">Definition & Funktion</div>
|
||
<p id="modalDefinition" class="text-sm sm:text-base text-slate-800 font-medium mt-1 leading-relaxed"></p>
|
||
</div>
|
||
|
||
<div class="bg-indigo-50/70 p-4 sm:p-5 rounded-2xl border border-indigo-100">
|
||
<div class="text-xs font-bold text-indigo-800 uppercase tracking-wider">Beispiele aus der Textanalyse</div>
|
||
<div id="modalExamples" class="text-sm sm:text-base font-serif italic text-slate-800 mt-1 space-y-1"></div>
|
||
</div>
|
||
|
||
<div class="bg-amber-50/70 p-4 sm:p-5 rounded-2xl border border-amber-200/80">
|
||
<div class="text-xs font-bold text-amber-800 uppercase tracking-wider flex items-center gap-1.5">
|
||
<i class="fa-solid fa-lightbulb"></i>
|
||
<span>Merkhilfe & Signalwörter</span>
|
||
</div>
|
||
<p id="modalTip" class="text-xs sm:text-sm text-amber-950 mt-1 leading-relaxed font-medium"></p>
|
||
</div>
|
||
|
||
<!-- Verwechslungsgefahr Sektion -->
|
||
<div id="modalConfusionContainer" class="bg-rose-50/70 p-4 sm:p-5 rounded-2xl border border-rose-200/80">
|
||
<div class="text-xs font-bold text-rose-800 uppercase tracking-wider flex items-center gap-1.5">
|
||
<i class="fa-solid fa-triangle-exclamation text-rose-600"></i>
|
||
<span>Achtung: Verwechslungsgefahr!</span>
|
||
</div>
|
||
<div id="modalConfusionTrap" class="font-extrabold text-sm sm:text-base text-rose-950 mt-1"></div>
|
||
<p id="modalConfusionText" class="text-xs sm:text-sm text-slate-700 mt-1 leading-relaxed"></p>
|
||
</div>
|
||
|
||
<!-- Klausur-Formulierungshilfe Sektion -->
|
||
<div id="modalAnalysisContainer" class="bg-indigo-50/70 p-4 sm:p-5 rounded-2xl border border-indigo-200/80">
|
||
<div class="flex items-center justify-between">
|
||
<div class="text-xs font-bold text-indigo-900 uppercase tracking-wider flex items-center gap-1.5">
|
||
<i class="fa-solid fa-pen-nib text-indigo-600"></i>
|
||
<span>Formulierungshilfe (Textanalyse)</span>
|
||
</div>
|
||
<button onclick="copyAnalysisTemplate()" id="copyAnalysisBtn" class="px-2.5 py-1 rounded-lg bg-indigo-100 hover:bg-indigo-200 text-indigo-800 text-xs font-bold transition flex items-center gap-1">
|
||
<i class="fa-regular fa-copy"></i>
|
||
<span id="copyBtnText">Kopieren</span>
|
||
</button>
|
||
</div>
|
||
<div id="modalAnalysisTemplate" class="mt-2 p-3 bg-white/90 rounded-xl border border-indigo-100 font-serif italic text-xs sm:text-sm text-slate-800 leading-relaxed select-all"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="pt-3 flex justify-between items-center">
|
||
<span class="text-xs text-slate-400 font-mono">[Esc] zum Schließen</span>
|
||
<button onclick="closeDetailModal()" class="px-6 py-2.5 rounded-xl bg-slate-900 hover:bg-slate-800 text-white font-bold text-sm sm:text-base transition shadow-sm">
|
||
Verstanden
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
<!-- SYNC & EXPORT MODAL -->
|
||
<div id="syncModal" 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-lg 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">
|
||
<button onclick="closeSyncModal()" 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="space-y-4">
|
||
<div class="flex items-center gap-2">
|
||
<span class="text-xs font-bold px-2.5 py-0.5 rounded-full bg-indigo-50 text-indigo-700 border border-indigo-200 uppercase tracking-wider">
|
||
Backup & Mobil-Sync
|
||
</span>
|
||
</div>
|
||
|
||
<div>
|
||
<h3 class="text-2xl sm:text-3xl font-black text-slate-900">Lernstand übertragen</h3>
|
||
<p class="text-xs sm:text-sm text-slate-500 mt-0.5">Synchronisiere deine gelernten Karten & Quiz-Statistiken nahtlos zwischen Geräten.</p>
|
||
</div>
|
||
|
||
<!-- Sync Stats Snapshot -->
|
||
<div class="p-3.5 bg-slate-50 rounded-2xl border border-slate-200 grid grid-cols-3 gap-2 text-center text-xs">
|
||
<div>
|
||
<div class="font-bold text-slate-400 uppercase text-[10px]">Karten gelernt</div>
|
||
<div id="syncStatMastered" class="font-black text-base text-indigo-600 mt-0.5">0 / 23</div>
|
||
</div>
|
||
<div>
|
||
<div class="font-bold text-slate-400 uppercase text-[10px]">Schwachstellen</div>
|
||
<div id="syncStatMistakes" class="font-black text-base text-rose-600 mt-0.5">0</div>
|
||
</div>
|
||
<div>
|
||
<div class="font-bold text-slate-400 uppercase text-[10px]">Max Streak</div>
|
||
<div id="syncStatStreak" class="font-black text-base text-amber-600 mt-0.5">0</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Sub Tabs for Sync: QR-Code | JSON | Code -->
|
||
<div class="flex rounded-xl bg-slate-100 p-1 text-xs font-bold border border-slate-200">
|
||
<button id="syncTabBtn-qr" onclick="switchSyncTab('qr')" class="flex-1 py-1.5 rounded-lg bg-white text-indigo-700 shadow-sm transition">
|
||
<i class="fa-solid fa-qrcode mr-1"></i> QR-Code
|
||
</button>
|
||
<button id="syncTabBtn-json" onclick="switchSyncTab('json')" class="flex-1 py-1.5 rounded-lg text-slate-600 hover:text-slate-900 transition">
|
||
<i class="fa-solid fa-file-code mr-1"></i> JSON-Datei
|
||
</button>
|
||
<button id="syncTabBtn-code" onclick="switchSyncTab('code')" class="flex-1 py-1.5 rounded-lg text-slate-600 hover:text-slate-900 transition">
|
||
<i class="fa-solid fa-terminal mr-1"></i> Code
|
||
</button>
|
||
</div>
|
||
|
||
<!-- TAB 1: QR CODE -->
|
||
<div id="syncSection-qr" class="space-y-3.5 text-center">
|
||
<div class="p-4 bg-white rounded-2xl border-2 border-indigo-100 inline-block mx-auto shadow-sm">
|
||
<div id="syncQrContainer" class="flex justify-center items-center min-w-[200px] min-h-[200px]">
|
||
<!-- QR Code image injected here -->
|
||
</div>
|
||
</div>
|
||
<p class="text-xs sm:text-sm text-slate-600 max-w-sm mx-auto">
|
||
Öffne die Kamera-App auf deinem Smartphone und scanne diesen QR-Code. Dein kompletter Lernstand wird sofort auf dem Handy geöffnet.
|
||
</p>
|
||
<div class="flex justify-center gap-2 pt-1">
|
||
<button onclick="copySyncUrl()" class="px-5 py-2.5 rounded-xl bg-indigo-600 hover:bg-indigo-700 text-white font-bold text-xs sm:text-sm shadow transition flex items-center gap-2">
|
||
<i class="fa-regular fa-copy"></i>
|
||
<span id="copySyncUrlBtnText">Sync-Link kopieren</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- TAB 2: JSON BACKUP -->
|
||
<div id="syncSection-json" class="hidden space-y-3">
|
||
<div class="p-4 bg-slate-50 rounded-2xl border border-slate-200 space-y-2 text-xs sm:text-sm">
|
||
<p class="text-slate-700">Sichere deinen Lernstand als Datei oder stelle ein bestehendes Backup wieder her:</p>
|
||
<div class="pt-2 flex flex-col sm:flex-row gap-2.5">
|
||
<button onclick="exportJsonFile()" class="flex-1 px-4 py-2.5 rounded-xl bg-slate-900 hover:bg-slate-800 text-white font-bold text-xs sm:text-sm transition flex items-center justify-center gap-2 shadow-sm">
|
||
<i class="fa-solid fa-download"></i>
|
||
<span>JSON herunterladen</span>
|
||
</button>
|
||
<label class="flex-1 px-4 py-2.5 rounded-xl bg-indigo-50 hover:bg-indigo-100 text-indigo-700 border border-indigo-200 font-bold text-xs sm:text-sm transition flex items-center justify-center gap-2 cursor-pointer shadow-sm">
|
||
<i class="fa-solid fa-upload"></i>
|
||
<span>JSON hochladen</span>
|
||
<input type="file" id="syncFileInput" accept=".json" onchange="importJsonFile(event)" class="hidden">
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- TAB 3: CODE -->
|
||
<div id="syncSection-code" class="hidden space-y-3 text-xs sm:text-sm">
|
||
<div>
|
||
<label class="font-bold text-slate-600 uppercase text-[11px]">Dein aktueller Sync-Code:</label>
|
||
<textarea id="syncExportText" readonly class="w-full mt-1 p-2.5 bg-slate-100 border border-slate-200 rounded-xl font-mono text-[11px] h-16 resize-none focus:outline-none select-all"></textarea>
|
||
<button onclick="copySyncToken()" class="mt-1 text-xs font-bold text-indigo-600 hover:text-indigo-800 flex items-center gap-1">
|
||
<i class="fa-regular fa-copy"></i> Code kopieren
|
||
</button>
|
||
</div>
|
||
|
||
<div class="pt-2 border-t border-slate-100">
|
||
<label class="font-bold text-slate-600 uppercase text-[11px]">Fremden Sync-Code einfügen:</label>
|
||
<textarea id="syncImportText" placeholder="Füge hier den kopierten Sync-Code ein..." class="w-full mt-1 p-2.5 bg-white border border-slate-300 rounded-xl font-mono text-[11px] h-16 resize-none focus:outline-none focus:ring-2 focus:ring-indigo-500"></textarea>
|
||
<button onclick="applyManualSyncToken()" class="mt-2 w-full py-2.5 rounded-xl bg-indigo-600 hover:bg-indigo-700 text-white font-bold text-xs sm:text-sm transition flex items-center justify-center gap-2 shadow-sm">
|
||
<i class="fa-solid fa-file-import"></i> Code anwenden
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="pt-2 border-t border-slate-100 flex justify-end">
|
||
<button onclick="closeSyncModal()" class="px-5 py-2 rounded-xl bg-slate-100 hover:bg-slate-200 text-slate-700 font-bold text-xs sm:text-sm transition">
|
||
Schließen
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- INCOMING SYNC CONFIRMATION MODAL -->
|
||
<div id="incomingSyncModal" 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-md w-full p-6 sm:p-7 shadow-2xl relative border border-slate-200 text-center space-y-4">
|
||
<div class="w-16 h-16 mx-auto rounded-2xl bg-indigo-100 text-indigo-600 flex items-center justify-center text-3xl shadow-inner">
|
||
<i class="fa-solid fa-arrows-rotate"></i>
|
||
</div>
|
||
<div>
|
||
<h3 class="text-xl sm:text-2xl font-black text-slate-900">Neuer Lernstand erkannt!</h3>
|
||
<p class="text-xs sm:text-sm text-slate-500 mt-1">Über den QR-Code / Link wurde ein externer Lernstand übertragen.</p>
|
||
</div>
|
||
|
||
<div class="bg-indigo-50/70 p-4 rounded-2xl border border-indigo-100 text-left text-xs sm:text-sm space-y-1.5">
|
||
<div class="flex justify-between">
|
||
<span class="text-slate-600">Gelernte Karten:</span>
|
||
<span id="incomingSyncMastered" class="font-bold text-indigo-900">-</span>
|
||
</div>
|
||
<div class="flex justify-between">
|
||
<span class="text-slate-600">Schwachstellen:</span>
|
||
<span id="incomingSyncMistakes" class="font-bold text-rose-700">-</span>
|
||
</div>
|
||
<div class="flex justify-between">
|
||
<span class="text-slate-600">Max Quiz Streak:</span>
|
||
<span id="incomingSyncStreak" class="font-bold text-amber-700">-</span>
|
||
</div>
|
||
</div>
|
||
|
||
<p class="text-xs text-slate-500">Möchtest du deinen aktuellen lokalen Speicher mit diesem Lernstand aktualisieren?</p>
|
||
|
||
<div class="flex gap-2.5 justify-center pt-2">
|
||
<button onclick="confirmIncomingSync()" class="px-5 py-2.5 rounded-xl bg-indigo-600 hover:bg-indigo-700 text-white font-bold text-xs sm:text-sm shadow transition">
|
||
Lernstand übernehmen
|
||
</button>
|
||
<button onclick="cancelIncomingSync()" class="px-4 py-2.5 rounded-xl bg-slate-100 hover:bg-slate-200 text-slate-700 font-bold text-xs sm:text-sm transition">
|
||
Abbrechen
|
||
</button>
|
||
</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 -->
|
||
<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">
|
||
<i id="toastIcon" class="fa-solid fa-circle-check text-emerald-400 text-base"></i>
|
||
<span id="toastMessage">Fortschritt gespeichert!</span>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
/* Complete Dataset with 10 Examples per Stilmittel, Traps & Formulations */
|
||
const STILMITTEL_DATA = [
|
||
{
|
||
"id": "allegorie",
|
||
"term": "Allegorie",
|
||
"category": "wort",
|
||
"categoryLabel": "Wortfigur / Trope",
|
||
"definition": "Verbildlichung von Abstraktem; eine konkrete Gestalt, Handlung oder Szene steht stellvertretend für einen abstrakten Begriff oder eine Idee.",
|
||
"examples": [
|
||
"Justitia mit Waage und Augenbinde (für die unvoreingenommene Gerechtigkeit)",
|
||
"Der Sensenmann / Gevatter Tod mit Sense und Stundenglas (für die unausweichliche Sterblichkeit)",
|
||
"Uncle Sam mit Zylinder und Zeigefinger 'I Want You' (für die Vereinigten Staaten von Amerika)",
|
||
"Das Staatsschiff, das durch stürmische Wogen gesteuert werden muss (für das Regieren eines Landes in Krisenzeiten)",
|
||
"Amor mit Pfeil und Bogen (für die plötzlich entflammende Liebe)",
|
||
"Der Schmarotzer / Parasit Neid, der an der Seele nagt (für zersetzende Missgunst)",
|
||
"Deutscher Michel mit Schlafmütze (nationale Allegorie für Naivität und Gemütlichkeit)",
|
||
"Das Rad der Fortuna / Schicksalsrad (für den ständigen Wandel von Glück und Elend)",
|
||
"Der Lebenslauf als beschwerlicher Berganstieg durch finstere Täler (für irdische Mühen)",
|
||
"Frau Welt mit schöner Gestalt vorn und Madenfrass am Rücken (mittelalterliche Allegorie der Vergänglichkeit)"
|
||
],
|
||
"highlighted": true,
|
||
"tip": "Achte auf personifizierte Symbolgestalten mit typischen Attributen (Waage, Sense) oder fortgeführte Metaphernreihen.",
|
||
"confusion": {
|
||
"trap": "Allegorie vs. Metapher vs. Personifikation",
|
||
"text": "Eine Metapher ist ein einzelnes punktuelles Sprachbild ('Mauer des Schweigens'). Eine Personifikation belebt einen Gegenstand kurzzeitig ('die Sonne lacht'). Eine Allegorie hingegen ist ein ganzes, systematisch ausgearbeitetes System oder eine eigenständige Figur, die einen abstrakten Begriff komplett verkörpert."
|
||
},
|
||
"analysisTemplate": "Durch die Allegorie [Figur, z. B. Justitia] verbildlicht der Verfasser die abstrakte Vorstellung von [Begriff, z. B. Gerechtigkeit] greifbar für das Publikum, wodurch das moralische Leitmotiv der Rede eindringlich verdeutlicht wird."
|
||
},
|
||
{
|
||
"id": "alliteration",
|
||
"term": "Alliteration",
|
||
"category": "klang",
|
||
"categoryLabel": "Klangfigur",
|
||
"definition": "Mehrere aufeinanderfolgende Wörter beginnen mit demselben Anlaut (betonten Konsonanten oder Vokal).",
|
||
"examples": [
|
||
"Milch macht müde Männer munter. (Klassischer Werbeslogan)",
|
||
"Fischers Fritz fischt frische Fische. (Zungenbrecher)",
|
||
"Kind und Kegel (Sprichwörtliche Zwillingsformel)",
|
||
"Haus und Hof / Hab und Gut (Rechts- und Sprachformel)",
|
||
"Winterstürme wichen dem Wonnemond. (Richard Wagner, Die Walküre)",
|
||
"Mars macht mobil bei Arbeit, Sport und Spiel. (Werbeslogan)",
|
||
"Bigger, Better, Burger King (Alliteration im Markenslogan)",
|
||
"Klipp und klar / Frank und frei (Feste Zwillingspaare)",
|
||
"Wind und Wetter / Schlicht und schlecht (Redensarten)",
|
||
"Veni, vidi, vici (Historische Lautfigur von Julius Caesar)"
|
||
],
|
||
"highlighted": true,
|
||
"tip": "Achte auf identische Anfangsbuchstaben bei betonten aufeinanderfolgenden Wörtern. Erzeugt Rhythmus, Wohlklang und Einprägsamkeit.",
|
||
"confusion": {
|
||
"trap": "Alliteration vs. Anapher vs. Assonanz",
|
||
"text": "Alliteration wiederholt denselben Anfangslaut bei benachbarten Wörtern ('Milch macht müde'). Anapher wiederholt ganze Wörter am Satzanfang ('Wir fordern... Wir fordern...'). Assonanz wiederholt nur Vokale im Wortinneren ('Ottos Mops kotzt')."
|
||
},
|
||
"analysisTemplate": "Die Alliteration '[Zitat]' stiftet durch den gleichförmigen Anlaut einen harmonischen Rhythmus, wodurch die Kernbotschaft eingängig verstärkt und im Gedächtnis des Rezipienten verankert wird."
|
||
},
|
||
{
|
||
"id": "anapher",
|
||
"term": "Anapher",
|
||
"category": "syntax",
|
||
"categoryLabel": "Satzfigur / Syntax",
|
||
"definition": "Wiederholung von einem oder mehreren Wörtern zu Beginn von mindestens zwei aufeinanderfolgenden Sätzen, Versen oder Satzteilen.",
|
||
"examples": [
|
||
"Das Wasser rauscht', das Wasser schwoll / Ein Fischer saß daran ... (Goethe, Der Fischer)",
|
||
"Wir fordern Gerechtigkeit. Wir fordern Freiheit. Wir fordern Zukunft. (Politische Rede)",
|
||
"Du hast mich x-mal belogen, du hast mich x-mal betrogen ... (Alltag / Songtext)",
|
||
"Was sag ich? Was klag ich? (Schiller)",
|
||
"I have a dream ... I have a dream today ... (Martin Luther King)",
|
||
"Geld ist nicht alles. Geld macht nicht glücklich. Geld beruhigt nur die Nerven. (Alltag)",
|
||
"Ihr Leben war Hingabe. Ihr Leben war Vorbild. Ihr Leben war Güte. (Nachruf)",
|
||
"Nicht hier, nicht jetzt, nicht unter diesen Bedingungen. (Verhandlung)",
|
||
"Wer nie sein Brot mit Tränen aß, / Wer nie die kummervollen Nächte ... (Goethe, Wilhelm Meister)",
|
||
"Heute stehen wir zusammen. Heute blicken wir nach vorn. Heute beginnen wir neu. (Ansprache)"
|
||
],
|
||
"highlighted": true,
|
||
"tip": "Prüfe den Beginn aufeinanderfolgender Sätze oder Verse. Häufigstes Stilmittel in Reden zur Steigerung der Dringlichkeit. Gegenteil: Epipher.",
|
||
"confusion": {
|
||
"trap": "Anapher vs. Epipher vs. Alliteration",
|
||
"text": "Anapher ist die Wiederholung am Satzanfang ('Ich kam... Ich sah...'). Epipher ist die Wiederholung am Satzende ('...alles gut, Ende gut'). Alliteration betrifft nur den ersten Laut einzelner Wörter ('frische Fische')."
|
||
},
|
||
"analysisTemplate": "Durch die Anapher '[wiederholtes Wort]' zu Beginn der Sätze baut der Autor eine drängende Dynamik auf, die dem Argument besonderen Nachdruck verleiht und das Publikum emotional mobilisiert."
|
||
},
|
||
{
|
||
"id": "antiklimax",
|
||
"term": "Antiklimax",
|
||
"category": "gedanke",
|
||
"categoryLabel": "Gedankenfigur",
|
||
"definition": "Einzelne Wörter oder Satzteile sind mit abnehmender Wichtigkeit, Intensität oder gesellschaftlichem Rang aneinandergereiht (Stufenfolge nach unten).",
|
||
"examples": [
|
||
"Kaiser, König, Edelmann; Bürger, Bauer, Bettelmann. (Historischer Abzählreim)",
|
||
"Doktoren, Magister, Schreiber und Pfaffen (Goethe, Faust I)",
|
||
"Wir verloren unser gesamtes Vermögen, das Haus und schließlich sogar den Regenschirm. (Alltagserzählung)",
|
||
"Für die Welt ein Held, für die Nation ein Führer, für seine Frau ein Pantoffelheld. (Spott)",
|
||
"In ganz Europa berühmt, im eigenen Land bekannt, in seiner Heimatstadt belächelt. (Ernüchterung)",
|
||
"Urahne, Großmutter, Mutter und Kind (Gustav Schwab, Das Gewitter)",
|
||
"Erst zerstörte das Unwetter die Ernte, dann riss es die Scheune ein und zuletzt fiel auch noch der Strom aus.",
|
||
"General, Hauptmann, Gefreiter, Rekrut (Militärische Rangabstufung)",
|
||
"Ein ganzer Kontinent, ein einzelner Staat, ein kleines Dorf, ein verlassener Hof.",
|
||
"Wir retteten das Manuskript, den Laptop und am Ende wenigstens die Kaffeetasse."
|
||
],
|
||
"highlighted": true,
|
||
"tip": "Achte auf eine stufenweise Abschwächung der Bedeutung, Macht oder Größe – oft genutzt für Ironie, Enttäuschung oder Ernüchterung.",
|
||
"confusion": {
|
||
"trap": "Antiklimax vs. Klimax vs. Akkumulation",
|
||
"text": "Klimax steigert die Intensität stufenweise nach oben ('Tage, Wochen, Monate'). Antiklimax stuft nach unten ab ('Kaiser, König, Bettelmann'). Akkumulation reiht Unterbegriffe ohne erkennbare Rangordnung aneinander ('Sonne, Mond und Sterne')."
|
||
},
|
||
"analysisTemplate": "Mit der Antiklimax ('[Zitat]') lässt der Sprecher die Bedeutsamkeit schrittweise absinken, wodurch ein ernüchternder und zugleich ironischer Effekt erzielt wird, der das vorangegangene Pathos entlarvt."
|
||
},
|
||
{
|
||
"id": "antithese",
|
||
"term": "Antithese",
|
||
"category": "gedanke",
|
||
"categoryLabel": "Gedankenfigur",
|
||
"definition": "Gegensätzliche Begriffe, Gedanken oder Behauptungen werden einander direkt gegenübergestellt.",
|
||
"examples": [
|
||
"Der Wahn ist kurz, die Reu ist lang. (Schiller, Das Lied von der Glocke)",
|
||
"Was dieser heute baut, reißt jener morgen ein. (Andreas Gryphius)",
|
||
"Friede den Hütten! Krieg den Palästen! (Georg Büchner, Der Hessische Landbote)",
|
||
"Heiß geliebt und tief gehasst. (Alltag & Lyrik)",
|
||
"Himmel und Hölle / Tag und Nacht (Archetypische Gegensätze)",
|
||
"Wo viel Licht ist, ist starker Schatten. (Goethe, Götz von Berlichingen)",
|
||
"Die Kunst ist lang, und kurz ist unser Leben. (Hippokrates / Goethe)",
|
||
"Gute Menschen reden wenig und handeln viel; schlechte reden viel und handeln wenig.",
|
||
"Arm an Beutel, reich an Geist. (Sprichwort)",
|
||
"Ein kleiner Schritt für einen Menschen, aber ein riesiger Sprung für die Menschheit. (Neil Armstrong)"
|
||
],
|
||
"highlighted": true,
|
||
"tip": "Zwei polare Gegenstücke treffen aufeinander. Betont Widersprüche, Dilemmata und innere Zerrissenheit.",
|
||
"confusion": {
|
||
"trap": "Antithese vs. Oxymoron vs. Paradoxon",
|
||
"text": "Antithese stellt zwei gegensätzliche Gedanken/Begriffe syntaktisch gegenüber ('Himmel und Hölle'). Oxymoron presst zwei Widersprüche direkt in eine Wortverbindung ('bittersüß', 'stummer Schrei'). Paradoxon ist ein scheinbar unlogischer Gesamtsatz mit Tiefsinn ('Ich weiß, dass ich nichts weiß')."
|
||
},
|
||
"analysisTemplate": "Durch die Antithese von '[Begriff A]' und '[Begriff B]' schärft der Verfasser den grundlegenden Konflikt, sodass die Unvereinbarkeit der beiden Positionen für den Leser unübersehbar hervortritt."
|
||
},
|
||
{
|
||
"id": "chiasmus",
|
||
"term": "Chiasmus",
|
||
"category": "syntax",
|
||
"categoryLabel": "Satzfigur / Syntax",
|
||
"definition": "Überkreuzstellung von einander entsprechenden Satzgliedern oder Wörtern (nach dem griechischen Buchstaben Chi: Χ).",
|
||
"examples": [
|
||
"Die Kunst ist lang, und kurz ist unser Leben. (Goethe, Faust I)",
|
||
"Ich schlafe am Tag, in der Nacht wache ich. (Sprachbeispiel)",
|
||
"Eng ist die Welt und das Gehirn ist weit. (Schiller, Wallenstein)",
|
||
"Der Einsatz war riesig, winzig war der Gewinn. (Kontrast)",
|
||
"Wer nicht will, was er kann, muss können, was er will.",
|
||
"Die Welt ist laut, still ist mein Herz.",
|
||
"Wir leben nicht, um zu essen, sondern wir essen, um zu leben. (Sokrates)",
|
||
"Einer für alle, alle für einen! (Alexandre Dumas, Die drei Musketiere)",
|
||
"Sie wissen nicht, was sie wollen, und was sie wollen, wissen sie nicht.",
|
||
"Vergessen, was schmerzte, und erinnern, was tröstete."
|
||
],
|
||
"highlighted": false,
|
||
"tip": "Formelschema: A - B : B - A (z. B. Subjekt Prädikat : Prädikat Subjekt). Wirkt geistreich, antithetisch und elegant.",
|
||
"confusion": {
|
||
"trap": "Chiasmus vs. Parallelismus",
|
||
"text": "Parallelismus wiederholt dieselbe grammatische Satzstruktur exakt parallel (A - B : A - B; z. B. 'Heiß ist die Liebe, kalt ist der Schnee'). Chiasmus überkreuzt sie spiegelbildlich (A - B : B - A; z. B. 'Die Kunst ist lang, und kurz ist unser Leben')."
|
||
},
|
||
"analysisTemplate": "Die chiastische Überkreuzstellung ('[Schema A-B : B-A]') spiegelt die inhaltliche Antithese formal in der Satzarchitektur wider und verleiht dem Argument eine pointierte, einprägsame Eleganz."
|
||
},
|
||
{
|
||
"id": "ellipse",
|
||
"term": "Ellipse",
|
||
"category": "syntax",
|
||
"categoryLabel": "Satzfigur / Syntax",
|
||
"definition": "Auslassung eines leicht ergänzbaren Satzteils (oft des Verbs), sodass der Satz grammatikalisch unvollständig, aber inhaltlich völlig verständlich bleibt.",
|
||
"examples": [
|
||
"Je früher [du kommst], desto besser [ist es]. (Feste Redewendung)",
|
||
"[Das] Ende gut, alles [ist] gut! (Sprichwort)",
|
||
"Erst [die] Arbeit, dann [das] Vergnügen! (Alltagssprache)",
|
||
"Was [soll ich] nun? Wohin [soll ich mich wenden]? (Dramatischer Monolog)",
|
||
"Ohne Wenn und Aber! (Alltägliche Bekräftigung)",
|
||
"Guten Morgen! [Ich wünsche dir einen guten Morgen.]",
|
||
"Rauchverbot! [Hier gilt ein striktes Rauchverbot.]",
|
||
"Zwei Kaffee, bitte! [Ich hätte gerne zwei Tassen Kaffee.]",
|
||
"Jung gewohnt, alt getan. (Sprichwort)",
|
||
"Feuer! Hilfe! Schnell! [Es brennt! Helft mir! Kommt schnell!]"
|
||
],
|
||
"highlighted": true,
|
||
"tip": "Spiegelt oft spontane Mündlichkeit, Atemlosigkeit, Hektik oder emotionale Erregung wider.",
|
||
"confusion": {
|
||
"trap": "Ellipse vs. Aposiopese vs. Zeugma",
|
||
"text": "Eine Ellipse lässt leicht ergänzbare Wörter (oft Verben) zur Verknappung weg ('Je schneller, desto besser'). Aposiopese bricht einen Satz mitten im Gedanken emotional ab ('Wenn ich dich erwische, dann...!'). Zeugma verbindet ein Verb mit zwei unpassenden Objekten ('Er nahm den Hut und Abschied')."
|
||
},
|
||
"analysisTemplate": "Die Ellipse ('[Zitat]') spiegelt die emotionale Erregung / Eile der Figur wider, beschleunigt das Lesetempo spürbar und konzentriert die Aussage auf das Wesentliche."
|
||
},
|
||
{
|
||
"id": "epipher",
|
||
"term": "Epipher",
|
||
"category": "syntax",
|
||
"categoryLabel": "Satzfigur / Syntax",
|
||
"definition": "Mehrere aufeinanderfolgende Sätze, Verse oder Teilsätze enden mit demselben Wort oder denselben Wörtern.",
|
||
"examples": [
|
||
"Unser Hotel: fantastisch! Unser Service: fantastisch! Unser Preis: fantastisch! (Werbung)",
|
||
"Ich liebe dich nicht mehr, ich begehre dich nicht mehr, ich ertrage dich nicht mehr. (Dramatischer Dialog)",
|
||
"Ende gut, alles gut. (Sprichwort)",
|
||
"Doch alle Lust will Ewigkeit –, / – will tiefe, tiefe Ewigkeit! (Nietzsche, Also sprach Zarathustra)",
|
||
"Er will alles. Er fordert alles. Er nimmt alles.",
|
||
"Mir geht es gut. Meinem Bruder geht es gut. Allen geht es gut. (Alltag)",
|
||
"Sie sprachen von Hoffnung, sie träumten von Hoffnung, doch starben ohne Hoffnung.",
|
||
"Nicht meine Schuld, nicht deine Schuld, es war niemandes Schuld.",
|
||
"Er versprach Freiheit, er kämpfte für Freiheit, er starb für die Freiheit. (Rede)",
|
||
"Gestern gelogen, heute gelogen, immer gelogen."
|
||
],
|
||
"highlighted": true,
|
||
"tip": "Verstärkt die Eindringlichkeit am Satzende und verleiht Aussagen einen feierlichen Nachklang. Gegenteil: Anapher.",
|
||
"confusion": {
|
||
"trap": "Epipher vs. Anapher vs. Endreim",
|
||
"text": "Epipher wiederholt dasselbe Wort am Satzende ('...gut, ...gut'). Anapher wiederholt am Satzanfang ('Gut ist..., Gut ist...'). Ein Endreim wiederholt nur den Gleichklang ab dem letzten betonten Vokal ('Haus' / 'Maus'), nicht das ganze Wort."
|
||
},
|
||
"analysisTemplate": "Die Epipher am Satzende ('[wiederholtes Wort]') hämmert den zentralen Begriff insistierend in das Bewusstsein des Publikums und verleiht dem Fazit einen rituellen, unumstößlichen Klang."
|
||
},
|
||
{
|
||
"id": "euphemismus",
|
||
"term": "Euphemismus",
|
||
"category": "wort",
|
||
"categoryLabel": "Wortfigur / Trope",
|
||
"definition": "Sprachliche Beschönigung, Milderung oder Verschleierung eines unangenehmen, tabuisierten oder beängstigenden Sachverhalts.",
|
||
"examples": [
|
||
"Freisetzung von Arbeitskräften (für Massenentlassung / Kündigung)",
|
||
"ableben / entschlafen / für immer die Augen schließen (für sterben)",
|
||
"Nullwachstum / negative Wachstumsrate (für Wirtschaftskrise / Rezession)",
|
||
"bildungsfern / sozial benachteiligt (für arm / ungebildet)",
|
||
"Seniorenresidenz (für Altenpflegeheim)",
|
||
"Kollateralschaden (für getötete Zivilisten im Kriegsgebiet)",
|
||
"Preisanpassung (für Preiserhöhung durch den Anbieter)",
|
||
"suboptimal / verbesserungswürdig (für mangelhaft oder misslungen)",
|
||
"aus zweiter Hand / pre-owned (für gebrauchte Ware)",
|
||
"vollschlank / stattlich gebaut (für übergewichtig)"
|
||
],
|
||
"highlighted": true,
|
||
"tip": "Wird bevorzugt in Politik, Verwaltung und PR genutzt, um harte Wahrheiten abzufedern.",
|
||
"confusion": {
|
||
"trap": "Euphemismus vs. Ironie vs. Litotes",
|
||
"text": "Ein Euphemismus soll die Realität tatsächlich milder klingen lassen ('entschlafen' statt 'sterben'). Ironie meint spöttisch das glatte Gegenteil ('tolles Wetter' bei Sturm). Litotes schwächt durch doppelte Verneinung ab ('nicht übel')."
|
||
},
|
||
"analysisTemplate": "Durch den Euphemismus '[beschönigender Begriff]' verschleiert der Redner die existenzielle Härte des eigentlichen Sachverhalts ('[reale Bedeutung]'), um das Publikum zu beruhigen und Kritik präventiv abzufedern."
|
||
},
|
||
{
|
||
"id": "hendiadyoin",
|
||
"term": "Hendiadyoin",
|
||
"category": "wort",
|
||
"categoryLabel": "Wortfigur / Zwillingsformel",
|
||
"definition": "Zwei gleichbedeutende oder sinnverwandte Wörter (meist Nomen) werden mit 'und' zu einer feststehenden Zwillingsformel mit Gesamtbedeutung verknüpft.",
|
||
"examples": [
|
||
"mit Fug und Recht (= mit voller Berechtigung)",
|
||
"in Bausch und Bogen (= vollständig, ohne Ausnahme)",
|
||
"Haus und Hof / Hab und Gut (= das gesamte Vermögen / Eigentum)",
|
||
"Feuer und Flamme sein (= restlos begeistert sein)",
|
||
"Gift und Galle spucken (= rasend vor Zorn sein)",
|
||
"Angst und Bange sein (= große Furcht empfinden)",
|
||
"Himmel und Hölle in Bewegung setzen (= alle denkbaren Hebel ansetzen)",
|
||
"in Amt und Würden (= in einer offiziellen Stellung / Würde)",
|
||
"Schimpf und Schande (= schwere Schmach und Demütigung)",
|
||
"klipp und klar feststellen (= unmissverständlich und eindeutig)"
|
||
],
|
||
"highlighted": false,
|
||
"tip": "Wörtlich 'eins durch zwei'. Traditionelle stehende Formeln der deutschen Sprache mit hohem Wiedererkennungswert.",
|
||
"confusion": {
|
||
"trap": "Hendiadyoin vs. Tautologie vs. Pleonasmus",
|
||
"text": "Ein Hendiadyoin ist eine traditionelle Paarformel mit 'und' ('Hab und Gut', 'Gift und Galle'), die gemeinsam eine feste Redewendungs-Bedeutung formt. Eine Tautologie häuft sinnverwandte Wörter ('nie und nimmer'). Ein Pleonasmus verdoppelt ein schon im Begriff enthaltenes Merkmal ('kaltes Eis')."
|
||
},
|
||
"analysisTemplate": "Die Zwillingsformel '[Ausdruck, z. B. mit Fug und Recht]' verleiht der Argumentation durch ihren festen, traditionsreichen Sprachklang eine selbstverständliche Glaubwürdigkeit und unanfechtbare Gültigkeit."
|
||
},
|
||
{
|
||
"id": "hyperbel",
|
||
"term": "Hyperbel",
|
||
"category": "wort",
|
||
"categoryLabel": "Wortfigur / Trope",
|
||
"definition": "Starke, oft ins Maßlose oder Unglaubwürdige gesteigerte sprachliche Übertreibung zur Emotionalisierung oder Zuspitzung.",
|
||
"examples": [
|
||
"Ein Meer von Tränen vergießen (Literarische Klage)",
|
||
"Ich habe dir das schon tausendmal gesagt! (Alltägliche Ermahnung)",
|
||
"Todmüde sein / im Schneckentempo kriechen / blitzschnell (Umgangssprache)",
|
||
"Ich sterbe vor Hunger! (Alltägliche Übertreibung)",
|
||
"Ich habe eine halbe Ewigkeit am Schalter gewartet!",
|
||
"Die Balken biegen sich vor dreisten Lügen.",
|
||
"Er besitzt Geld wie Heu und Reichtümer ohne Ende.",
|
||
"Ein unüberwindbarer Berg von Arbeit türmt sich vor mir auf.",
|
||
"Zu Tode erschrocken sein vor Schreck.",
|
||
"Sie weinte so herzzerreißend, dass die Wände wackelten."
|
||
],
|
||
"highlighted": true,
|
||
"tip": "Wörtlich genommen absurd oder unmöglich, soll aber das subjektive Gefühl ins Extreme heben.",
|
||
"confusion": {
|
||
"trap": "Hyperbel vs. Litotes vs. Metapher",
|
||
"text": "Hyperbel ist extreme sprachliche Übertreibung ('tausendmal'). Litotes ist das Gegenteil: maßlose Untertreibung bzw. doppelte Verneinung ('nicht uninteressant'). Eine Metapher überträgt Bedeutungen ohne zwingende Übertreibung."
|
||
},
|
||
"analysisTemplate": "Durch die maßlose Hyperbel ('[Zitat]') dramatisiert der Autor die Lage extrem, wodurch die subjektive Verzweiflung der Figur für den Leser unmittelbar nachfühlbar wird."
|
||
},
|
||
{
|
||
"id": "ironie",
|
||
"term": "Ironie",
|
||
"category": "gedanke",
|
||
"categoryLabel": "Gedankenfigur",
|
||
"definition": "Feiner Spott; man meint das genaue Gegenteil von dem, was wörtlich formuliert wird (erkennbar an Kontext, Mimik oder Tonfall).",
|
||
"examples": [
|
||
"Das hast du ja mal wieder ganz toll hinbekommen! (nach einem Missgeschick)",
|
||
"Schönes Wetter heute! (bei strömendem Platzregen)",
|
||
"Du bist mir ja ein wahrer Meisterdenker. (Spott über Unvernunft)",
|
||
"Lob der Faulheit (Lessing, satirisches Gedicht)",
|
||
"Nur keine Eile, wir warten ja erst seit zwei Stunden auf dich!",
|
||
"Eine wahre Delikatesse! (über ein völlig verbranntes Essen)",
|
||
"Er glänzte in der Besprechung wieder durch vollkommene Abwesenheit.",
|
||
"Das hat uns jetzt gerade noch zu unserem Glück gefehlt!",
|
||
"Du bist ja pünktlich wie die Eisenbahn im tiefen Schneesturm.",
|
||
"Großartige Idee, die Haustür bei Nacht weit offen stehen zu lassen!"
|
||
],
|
||
"highlighted": true,
|
||
"tip": "Der Empfänger muss den Code knacken. Kann humorvoll, kritisch oder verletzend (als Sarkasmus) wirken.",
|
||
"confusion": {
|
||
"trap": "Ironie vs. Sarkasmus vs. Zynismus",
|
||
"text": "Ironie ist die stilistische Grundform des verkehrten Sinns ('Tolles Wetter'). Sarkasmus ist beißende, aggressive und persönlich verletzende Ironie. Zynismus ist eine verachtende, tabubrechende Grundhaltung gegenüber Werten."
|
||
},
|
||
"analysisTemplate": "Die Ironie in der Aussage ('[Zitat]') erzeugt eine spöttische Distanz zum Geschehen und regt den Leser dazu an, die fragwürdige Haltung der Figur kritisch zu hinterfragen."
|
||
},
|
||
{
|
||
"id": "klimax",
|
||
"term": "Klimax",
|
||
"category": "gedanke",
|
||
"categoryLabel": "Gedankenfigur",
|
||
"definition": "Sich stufenweise steigernde Reihe von Wörtern oder Satzteilen (vom Schwächeren zum Stärkeren, Bedeutenderen).",
|
||
"examples": [
|
||
"Er kam, sah und siegte. (Veni, vidi, vici – Julius Caesar)",
|
||
"Heute back ich, morgen brau ich, übermorgen hol ich der Königin ihr Kind. (Gebrüder Grimm, Rumpelstilzchen)",
|
||
"Für diese Stadt, für unser Land, für die ganze Menschheit! (Politische Rede)",
|
||
"Tage, Wochen, Monate vergingen ohne jedes Lebenszeichen.",
|
||
"Gut, besser, am besten! (Werbliche Dreisatz-Steigerung)",
|
||
"Ich bitte Sie, ich beschwöre Sie, ich flehe Sie auf Knien an!",
|
||
"Er schluchzte leise, er weinte bitterlich, er schrie vor Schmerz.",
|
||
"Ein kurzer Blick, ein flüchtiges Wort, ein inniger Kuss.",
|
||
"Nicht bloß ein Vergehen, nicht nur ein Verbrechen, sondern nackter Hochverrat!",
|
||
"Wir müssen handeln: heute, in dieser Stunde, sofort!"
|
||
],
|
||
"highlighted": true,
|
||
"tip": "Baut systematische Dramatik und Spannung auf. Meist dreigliedrig angelegt. Gegenteil: Antiklimax.",
|
||
"confusion": {
|
||
"trap": "Klimax vs. Antiklimax vs. Trikolon",
|
||
"text": "Klimax ist eine Steigerung nach oben (schwach -> stark). Antiklimax geht stufenweise nach unten (Kaiser -> Bettelmann). Ein Trikolon ist ein dreigliedriger Satzbau, der nicht zwingend eine Steigerung enthalten muss (z. B. 'quadratisch, praktisch, gut')."
|
||
},
|
||
"analysisTemplate": "Die dreigliedrige Klimax ('[Stufenfolge]') baut eine zielgerichtete emotionale Spannung auf, die das Publikum sukzessive mitreißt und im finalen Höhepunkt kulminiert."
|
||
},
|
||
{
|
||
"id": "metapher",
|
||
"term": "Metapher",
|
||
"category": "wort",
|
||
"categoryLabel": "Wortfigur / Trope",
|
||
"definition": "Bildhafter Ausdruck; direkte Bedeutungsübertragung aus einem Herkunftsbereich in einen anderen ohne das Vergleichswort 'wie'.",
|
||
"examples": [
|
||
"Eine Mauer des Schweigens durchbrechen (Politik / Gesellschaft)",
|
||
"Das Wüstenschiff (für Kamel)",
|
||
"Jemandem das Herz brechen / Rabenmutter / Bücherwurm (Alltagsmetaphern)",
|
||
"Ein Meer aus Lichtern erhellte die dunkle Nacht.",
|
||
"Auf Wolke sieben schweben (für überglücklich / verliebt sein)",
|
||
"Schnee von gestern sein (für eine längst erledigte Sache)",
|
||
"Eine Flut von Beschwerden brach über die Behörde herein.",
|
||
"Das Licht der Wahrheit vertrieb die Schatten des Zweifels.",
|
||
"Der Geduldsfaden riss mit einem lauten Knall entzwei.",
|
||
"Er nagelte seinen Kontrahenten auf dessen frühere Aussage fest."
|
||
],
|
||
"highlighted": true,
|
||
"tip": "Ein 'Vergleich ohne wie'. Das Bild (Bildspender) verschmilzt nahtlos mit der Sache (Bildempfänger).",
|
||
"confusion": {
|
||
"trap": "Metapher vs. Vergleich vs. Symbol",
|
||
"text": "Vergleich hat immer ein 'wie' oder 'als' ('stark wie ein Bär'). Metapher lässt 'wie' weg ('Bärenkräfte', 'Rabenmutter'). Ein Symbol ist ein konkreter Gegenstand mit traditioneller geistiger Bedeutung (z. B. Kreuz für Glaube, Rose für Liebe)."
|
||
},
|
||
"analysisTemplate": "Die Metapher '[Ausdruck]' projiziert Eigenschaften aus dem Vorstellungsbereich [Bereich A] auf [Bereich B], wodurch dem Rezipienten die [Wirkung] plastisch und ohne Umschweife vor Augen geführt wird."
|
||
},
|
||
{
|
||
"id": "neologismus",
|
||
"term": "Neologismus",
|
||
"category": "wort",
|
||
"categoryLabel": "Wortfigur / Neubildung",
|
||
"definition": "Sprachliche Neubildung von Begriffen oder Wörtern, die neu geprägt und in den Sprachgebrauch eingebracht werden.",
|
||
"examples": [
|
||
"Klimakleber / Schwurbeln / Technologiepark (Aktuelle Sprachneubildungen)",
|
||
"Knabenmorgen-Blütenträume (Goethe, Prometheus)",
|
||
"Fake News / googeln / chillen / unkaputtbar (Umgangssprache & Werbesprache)",
|
||
"Nebelglanz / Waldeinsamkeit (Romantische Wortschöpfung, Tieck)",
|
||
"Verschlimmbessern (ironische Wortzusammensetzung)",
|
||
"Kaufrausch / Cyberkrieg / Enkeltrick (Moderne Wortprägungen)",
|
||
"Infodemie (Flut unkontrollierter Gerüchte in Krisen)",
|
||
"Gönnung / lost sein (Jugendwörter der Moderne)",
|
||
"Warmduscher / Schattenparker (Umgangssprachliche Schöpfungen)",
|
||
"Geisterspiel (Sportveranstaltung ohne Publikum im Stadion)"
|
||
],
|
||
"highlighted": true,
|
||
"tip": "Zeigt Zeitgeist, Jugendkultur, gesellschaftliche Debatten oder poetische Schöpferkraft an.",
|
||
"confusion": {
|
||
"trap": "Neologismus vs. Archaismus vs. Anglizismus",
|
||
"text": "Neologismus ist eine Neuprägung eines Begriffs. Ein Anglizismus ist ein Lehnwort aus dem Englischen (das nicht neu sein muss). Ein Archaismus ist das Gegenteil: ein bewusst veralteter, historischer Ausdruck ('Minne', 'Weyb')."
|
||
},
|
||
"analysisTemplate": "Die Wortneuschöpfung '[Neologismus]' greift aktuelle Zeitströmungen auf und signalisiert dem Leser Modernität / ironische Distanz gegenüber herkömmlichen Denkmustern."
|
||
},
|
||
{
|
||
"id": "onomatopoesie",
|
||
"term": "Onomatopoesie",
|
||
"category": "klang",
|
||
"categoryLabel": "Klangfigur",
|
||
"definition": "Lautmalerei bzw. sprachliche Nachahmung von natürlichen Lauten, Tiergeräuschen oder technischen Tönen.",
|
||
"examples": [
|
||
"Knistern, Knacken, Rauschen, Kuckuck (Lautnachahmende Wörter)",
|
||
"Summ, summ, summ! Bienchen summ herum! (Kinderlied)",
|
||
"Zack! Bumm! Peng! (Comicsprache)",
|
||
"Es wallet und siedet und brauset und zischt ... (Schiller, Der Taucher)",
|
||
"Das Ticken der Standuhr im leeren Raum: Tick-Tack, Tick-Tack.",
|
||
"Die Blätter rascheln leise im sanften Abendwind.",
|
||
"Miau, Wau-Wau, Kikeriki! (Tierlautnachahmungen)",
|
||
"Das stetige Plätschern, Tropfen und Rinnen des Gebirgsbaches.",
|
||
"Das nervtötende Summen und Brummen der alten Maschine.",
|
||
"Klirrend zerschellte die Kristallvase auf den Fliesen."
|
||
],
|
||
"highlighted": true,
|
||
"tip": "Macht das Beschriebene akustisch sinnlich greifbar (oft in Lyrik, Balladen und Comics).",
|
||
"confusion": {
|
||
"trap": "Onomatopoesie vs. Alliteration",
|
||
"text": "Onomatopoesie ahmt den tatsächlichen Klang eines Geräuschs lautmalerisch nach ('Knistern', 'Miau', 'Zischen'). Alliteration wiederholt lediglich den gleichen Anfangsbuchstaben bei Wörtern ('frische Fische'), ohne zwingend ein Geräusch nachzuahmen."
|
||
},
|
||
"analysisTemplate": "Durch die lautmalerische Onomatopoesie ('[Lautmalerei]') wird das Geschehen akustisch unmittelbar erlebbar, sodass der Leser das [Geräusch/Atmosphäre] sinnesnah mitempfindet."
|
||
},
|
||
{
|
||
"id": "oxymoron",
|
||
"term": "Oxymoron",
|
||
"category": "gedanke",
|
||
"categoryLabel": "Gedankenfigur",
|
||
"definition": "Verbindung zweier Vorstellungen oder Begriffe, die sich logisch direkt widersprechen oder gegenseitig ausschließen.",
|
||
"examples": [
|
||
"Beredtes Schweigen / Offenes Geheimnis (Klassische Oxymora)",
|
||
"Alter Knabe / Bittere Süße / Stiller Schrei (Literatur & Alltag)",
|
||
"Eile mit Weile (Festes Sprichwort)",
|
||
"Schwarze Milch der Frühe (Paul Celan, Todesfuge)",
|
||
"Hassliebe / bittersüß empfunden (Widersprüchliche Gefühle)",
|
||
"Eiskalte Hitze durchzog seine Adern.",
|
||
"Grausame Güte / teuflisch gut gelungen",
|
||
"Flüssiges Eis / lebendige Leiche",
|
||
"Traurigfroh blickte der Veteran in den Himmel.",
|
||
"Stummer Zeuge / laute Stille"
|
||
],
|
||
"highlighted": false,
|
||
"tip": "Meist eine kompakte Adjektiv-Nomen-Kombination. Erzeugt Verblüffung und zwingt zum Innehalten.",
|
||
"confusion": {
|
||
"trap": "Oxymoron vs. Antithese vs. Paradoxon",
|
||
"text": "Ein Oxymoron komprimiert den Widerspruch in zwei eng zusammenhängende Wörter ('bittersüß', 'alter Knabe'). Die Antithese stellt zwei getrennte Satzglieder gegenüber ('Der Wahn ist kurz, die Reu ist lang'). Das Paradoxon ist ein vollständiger, scheinbar widersinniger Sinnspruch."
|
||
},
|
||
"analysisTemplate": "Das Oxymoron ('[Kombination]') prallt als logischer Widerspruch aufeinander und bringt die innere Zerrissenheit / unauflösbare Ambivalenz der Situation treffend zum Ausdruck."
|
||
},
|
||
{
|
||
"id": "paradoxon",
|
||
"term": "Paradoxon",
|
||
"category": "gedanke",
|
||
"categoryLabel": "Gedankenfigur",
|
||
"definition": "Eine scheinbar widersprüchliche oder widersinnige Aussage, die bei genauerem Hinsehen eine tiefere, oft überraschende Wahrheit birgt.",
|
||
"examples": [
|
||
"Ich weiß, dass ich nichts weiß. (Sokrates)",
|
||
"Weniger ist mehr. (Mies van der Rohe / Designleitsatz)",
|
||
"Umsonst ist der Tod, und der kostet das Leben. (Sprichwort)",
|
||
"Wer sein Leben gewinnen will, der wird es verlieren. (Bibel, Matthäus 16,25)",
|
||
"Im scheinbaren Rückschritt liegt oft der entscheidende Fortschritt.",
|
||
"Man muss erst investieren und Geld ausgeben, um Geld zu sparen.",
|
||
"Je mehr der Mensch lernt, desto deutlicher spürt er sein Nichtwissen.",
|
||
"Der Weg ist das Ziel. (Konfuzius / Östliche Philosophie)",
|
||
"Die Ausnahme bestätigt die Regel. (Sprichwörtliches Paradox)",
|
||
"Manchmal kann ein beredtes Schweigen lauter dröhnen als jedes Wort."
|
||
],
|
||
"highlighted": false,
|
||
"tip": "Im Unterschied zum zweigliedrigen Oxymoron meist ein ganzer zusammenhängender Gedanke oder Satz.",
|
||
"confusion": {
|
||
"trap": "Paradoxon vs. Oxymoron",
|
||
"text": "Ein Paradoxon ist eine ganze Aussage oder Behauptung mit tiefgründiger Scheinlogik ('Wer sein Leben liebt, verliert es'). Ein Oxymoron ist lediglich eine punktuelle Verbindung zweier Wörter ('stilles Geschrei')."
|
||
},
|
||
"analysisTemplate": "Das Paradoxon ('[Aussage]') durchbricht die Erwartungshaltung des Lesers durch einen scheinbaren Widerspruch und provoziert ihn dazu, über die tiefere philosophische Wahrheit der These nachzudenken."
|
||
},
|
||
{
|
||
"id": "pars_pro_toto",
|
||
"term": "Pars pro Toto",
|
||
"category": "wort",
|
||
"categoryLabel": "Wortfigur / Trope",
|
||
"definition": "Ein Teil steht stellvertretend für das Ganze (Sonderform der Synekdoche) oder eine einzelne Institution für ein Gesamtsystem.",
|
||
"examples": [
|
||
"Ein Dach über dem Kopf haben (für ein ganzes Haus / eine schützende Wohnung)",
|
||
"Pro Kopf berechnen (für pro Person / pro Mensch)",
|
||
"Brüssel beschließt neue Richtlinien (für die Europäische Union samt Organen)",
|
||
"Er schwingt die scharfe Klinge (für das gesamte Schwert im Kampf)",
|
||
"Brot für die Welt erbitten (Brot stellvertretend für alle Nahrungsmittel)",
|
||
"Viele kluge Köpfe grübelten über der Lösung (Köpfe für Wissenschaftler/Experten)",
|
||
"Wir brauchen auf der Baustelle jede helfende Hand (Hand für ganze Arbeitskräfte)",
|
||
"Das Weiße Haus weist die Anschuldigungen zurück (für den US-Präsidenten und Stab)",
|
||
"Die stolzen Segel verließen den Hafen (Segel für die gesamten Segelschiffe)",
|
||
"Räder müssen rollen für den Aufschwung (Räder stellvertretend für den gesamten Verkehr)"
|
||
],
|
||
"highlighted": false,
|
||
"tip": "Wörtlich lateinisch 'ein Teil für das Ganze'. Sehr häufig in Medien, Alltag und politischer Berichterstattung.",
|
||
"confusion": {
|
||
"trap": "Pars pro Toto vs. Totum pro Parte vs. Metonymie",
|
||
"text": "Pars pro Toto nutzt einen Teil für das Ganze ('Dach überm Kopf' = Haus). Totum pro Parte tut das Umgekehrte: das Ganze für einen Teil ('Deutschland gewinnt gegen Spanien' = die Fußballmannschaft). Metonymie ersetzt einen Begriff durch einen sachlich verwandten ('das Weiße Haus' für den Präsidenten)."
|
||
},
|
||
"analysisTemplate": "Mit dem Stilmittel Pars pro Toto ('[Teilbegriff]' für '[Gesamtbegriff]') lenkt der Text die Aufmerksamkeit gezielt auf das funktionale Detail und erzeugt eine anschauliche Verknappung."
|
||
},
|
||
{
|
||
"id": "personifikation",
|
||
"term": "Personifikation",
|
||
"category": "wort",
|
||
"categoryLabel": "Wortfigur / Trope",
|
||
"definition": "Vermenschlichung einer Idee, einer Sache oder eines Naturphänomens; Abstraktes oder Unbelebtes handelt oder fühlt wie eine Person.",
|
||
"examples": [
|
||
"Die Zeit rennt / Die Sonne lacht / Der Himmel weint (Alltagssprache)",
|
||
"Der Wind heult durch die Gassen und rüttelt an den Türen. (Erzählung)",
|
||
"Mutter Natur schläft im eisigen Winter. (Naturlyrik)",
|
||
"Die Gerechtigkeit fordert ihr unbarmherziges Opfer. (Drama / Rede)",
|
||
"Die kalte Angst packte ihn mit eisigen Klauen an der Kehle.",
|
||
"Das Glück klopfte eines Tages unverhofft an seine Pforte.",
|
||
"Der alte Motor stöhnte gequält und gab schließlich den Geist auf.",
|
||
"Die tosenden Meereswellen verschlangen das wehrlose Fischerboot.",
|
||
"Die unbestechliche Geschichte lehrt uns immer dieselben Lehren.",
|
||
"Die finstere Nacht hüllte die schlafende Stadt in ihren Mantel."
|
||
],
|
||
"highlighted": true,
|
||
"tip": "Verleiht Dingen Organe (Auge des Gesetzes), Gefühle (lachen, weinen) oder Handlungen (rennt, heult).",
|
||
"confusion": {
|
||
"trap": "Personifikation vs. Allegorie vs. Metapher",
|
||
"text": "Personifikation verleiht Gegenständen menschliche Verben/Eigenschaften ('die Zeit rennt'). Allegorie ist eine voll ausgearbeitete Person als Symbol (die Figur Justitia). Metapher überträgt Bildhaftigkeit ohne zwingende Vermenschlichung ('Geduldsfaden gerissen')."
|
||
},
|
||
"analysisTemplate": "Durch die Personifikation von '[Gegenstand/Naturkraft]' ('[menschliche Aktion]') erwacht das leblose Umfeld dynamisch zum Leben, was die Bedrohlichkeit / Harmonie der Szene emotional spürbar macht."
|
||
},
|
||
{
|
||
"id": "rhetorische_frage",
|
||
"term": "Rhetorische Frage",
|
||
"category": "gedanke",
|
||
"categoryLabel": "Gedankenfigur",
|
||
"definition": "Scheinfrage, auf die keine Antwort erwartet wird, weil die Antwort ohnehin offensichtlich oder unausweichlich ist.",
|
||
"examples": [
|
||
"Wer weiß das schon? / Wer möchte nicht glücklich sein? (Alltag)",
|
||
"Machen wir nicht alle einmal Fehler? (Rede zur Versöhnung)",
|
||
"Bist du denn völlig verrückt geworden? (Umgangssprache)",
|
||
"Habe ich dir das nicht von Anfang an prophezeit? (Vorwurf)",
|
||
"Sehe ich etwa aus wie eine Goldgrube? (Familiärer Spruch)",
|
||
"Wie lange wollen wir diesem Unrecht noch tatenlos zuschauen? (Politische Rede)",
|
||
"Ist das im Ernst dein voller Ernst?",
|
||
"Wer zweifelt denn ernsthaft an der Notwendigkeit dieser Maßnahme?",
|
||
"Was gibt es Herrlicheres als einen sonnigen Urlaubstag am Meer?",
|
||
"Glaubst du im Ernst, dass Geldscheine auf den Bäumen wachsen?"
|
||
],
|
||
"highlighted": true,
|
||
"tip": "Dient der Beeinflussung des Publikums und stellt stillschweigendes Einvernehmen her.",
|
||
"confusion": {
|
||
"trap": "Rhetorische Frage vs. Suggestivfrage",
|
||
"text": "Eine rhetorische Frage verlangt überhaupt keine Antwort, weil die Zustimmung stillschweigend vorausgesetzt wird ('Wer liebt nicht die Freiheit?'). Eine Suggestivfrage zwingt den Gesprächspartner durch geschickte Formulierung zu einer gewünschten Antwort ('Finden Sie nicht auch, dass Herr Müller schuld ist?')."
|
||
},
|
||
"analysisTemplate": "Die rhetorische Frage ('[Zitat]') bindet den Zuhörer aktiv ein und drängt ihn suggestiv dazu, der unausweichlichen Folgerung des Sprechers innerlich beizupflichten."
|
||
},
|
||
{
|
||
"id": "tautologie_pleonasmus",
|
||
"term": "Tautologie oder Pleonasmus",
|
||
"category": "wort",
|
||
"categoryLabel": "Wortfigur / Häufung",
|
||
"definition": "Zwei Wörter, die denselben Sinninhalt ausdrücken, aber gemeinsam keine neue Bedeutung ergeben (Häufung sinnverwandter Wörter zur Verstärkung).",
|
||
"examples": [
|
||
"Der weiße Schimmel / Das nasse Wasser / Alter Greis (Pleonasmus: Eigenschaft steckt schon im Wort)",
|
||
"Voll und ganz / Nie und nimmer / Für und wider (Tautologie: Zwei sinnverwandte Wörter gleicher Wortart)",
|
||
"Bereits schon / Künftige Zukunft / Rückantwort (Umgangssprachliche Dopplungen)",
|
||
"Ende, Schluss und vorbei! (Umgangssprachliche Dreifachung)",
|
||
"Kaltes Eis / brennend heißes Feuer (Pleonasmus)",
|
||
"Immer und ewig / Art und Weise (Tautologische Zwillingsformeln)",
|
||
"Ein winzig kleiner Zwerg / weibliche Bundeskanzlerin (Pleonasmus)",
|
||
"Einzigste Alternative (Doppelte Steigerungsform)",
|
||
"Persönliche Anwesenheit ist zwingend erforderlich (Pleonasmus)",
|
||
"Still und leise schlich sich die Gestalt davon (Tautologie)"
|
||
],
|
||
"highlighted": false,
|
||
"tip": "Pleonasmus: Merkmal steckt schon im Hauptwort ('kaltes Eis'). Tautologie: Wiederholung mit derselben Wortart ('voll und ganz').",
|
||
"confusion": {
|
||
"trap": "Pleonasmus vs. Tautologie vs. Hendiadyoin",
|
||
"text": "Beim Pleonasmus ist das Beiwort im Grundwort schon logisch enthalten ('weißer Schimmel'). Die Tautologie wiederholt denselben Sinn mit gleichartigen Wörtern ('nie und nimmer'). Das Hendiadyoin verschmilzt zwei Begriffe zu einer festen, neuen Redensart ('Hab und Gut')."
|
||
},
|
||
"analysisTemplate": "Die nachdrückliche Häufung '[Ausdruck]' verstärkt den Aussagewert redundant, um unmissverständlich klarzustellen, dass der Sachverhalt in vollem Umfang zutrifft."
|
||
},
|
||
{
|
||
"id": "vergleich",
|
||
"term": "Vergleich",
|
||
"category": "wort",
|
||
"categoryLabel": "Wortfigur / Vergleich",
|
||
"definition": "Verknüpfung zweier Bedeutungsbereiche durch ein ausdrückliches Vergleichswort ('wie' oder 'als') über ein gemeinsames Merkmal.",
|
||
"examples": [
|
||
"Stark wie ein Löwe / Schnell wie der Blitz (Alltägliche Vergleiche)",
|
||
"Frei wie der Wind / Fleißig wie eine Biene (Sprichwörter)",
|
||
"Schlauer als die Polizei erlaubt (Feste Redensart)",
|
||
"Wie ein Fels in der Brandung stand er unerschütterlich da.",
|
||
"Er kämpfte wie ein Löwe um jeden einzelnen Zentimeter Boden.",
|
||
"Dumm wie Bohnenstroh / Arm wie eine Kirchenmaus (Redensarten)",
|
||
"Klar wie Kloßbrühe vor Augen liegen (Umgangssprache)",
|
||
"Sie strahlte wie die Morgensonne nach einem Regenschauer.",
|
||
"Ein Tag ohne Lächeln ist wie ein verlorener Tag. (Charlie Chaplin)",
|
||
"Weiß wie Schnee, rot wie Blut und schwarz wie Ebenholz. (Gebrüder Grimm, Schneewittchen)"
|
||
],
|
||
"highlighted": true,
|
||
"tip": "Immer am Signalwort 'wie' oder 'als' erkennbar. Fehlt das 'wie', entsteht eine Metapher!",
|
||
"confusion": {
|
||
"trap": "Vergleich vs. Metapher",
|
||
"text": "Der Vergleich hat IMMER ein explizites Vergleichswort wie 'wie' oder 'als' ('stark wie ein Löwe'). Fehlt das Vergleichswort und wird das Bild direkt übertragen ('Löwenmut', 'er ist ein Löwe im Kampf'), handelt es sich um eine Metapher!"
|
||
},
|
||
"analysisTemplate": "Durch den anschaulichen Vergleich '[Element A] wie [Element B]' wird die gemeinsame Eigenschaft des [gemeinsamer Punkt] plastisch hervorgehoben und dem Rezipienten vertraut und greifbar vermittelt."
|
||
}
|
||
];
|
||
|
||
// Web Audio Synthesizer Engine
|
||
let soundEnabled = localStorage.getItem('rhetorik_sound_enabled') !== 'false';
|
||
let audioCtx = null;
|
||
|
||
function getAudioContext() {
|
||
if (!audioCtx) {
|
||
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
||
if (AudioContext) audioCtx = new AudioContext();
|
||
}
|
||
if (audioCtx && audioCtx.state === 'suspended') {
|
||
audioCtx.resume();
|
||
}
|
||
return audioCtx;
|
||
}
|
||
|
||
function playTone(type) {
|
||
if (!soundEnabled) return;
|
||
try {
|
||
const ctx = getAudioContext();
|
||
if (!ctx) return;
|
||
const now = ctx.currentTime;
|
||
|
||
if (type === 'flip') {
|
||
const osc = ctx.createOscillator();
|
||
const gain = ctx.createGain();
|
||
osc.type = 'sine';
|
||
osc.frequency.setValueAtTime(320, now);
|
||
osc.frequency.exponentialRampToValueAtTime(560, now + 0.08);
|
||
gain.gain.setValueAtTime(0.06, now);
|
||
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.08);
|
||
osc.connect(gain);
|
||
gain.connect(ctx.destination);
|
||
osc.start(now);
|
||
osc.stop(now + 0.08);
|
||
} else if (type === 'success') {
|
||
[523.25, 659.25, 783.99].forEach((freq, idx) => {
|
||
const osc = ctx.createOscillator();
|
||
const gain = ctx.createGain();
|
||
osc.type = 'triangle';
|
||
osc.frequency.setValueAtTime(freq, now + idx * 0.06);
|
||
gain.gain.setValueAtTime(0.12, now + idx * 0.06);
|
||
gain.gain.exponentialRampToValueAtTime(0.001, now + idx * 0.06 + 0.18);
|
||
osc.connect(gain);
|
||
gain.connect(ctx.destination);
|
||
osc.start(now + idx * 0.06);
|
||
osc.stop(now + idx * 0.06 + 0.18);
|
||
});
|
||
} else if (type === 'error') {
|
||
[330, 246.94].forEach((freq, idx) => {
|
||
const osc = ctx.createOscillator();
|
||
const gain = ctx.createGain();
|
||
osc.type = 'sawtooth';
|
||
osc.frequency.setValueAtTime(freq, now + idx * 0.1);
|
||
gain.gain.setValueAtTime(0.1, now + idx * 0.1);
|
||
gain.gain.exponentialRampToValueAtTime(0.001, now + idx * 0.1 + 0.16);
|
||
osc.connect(gain);
|
||
gain.connect(ctx.destination);
|
||
osc.start(now + idx * 0.1);
|
||
osc.stop(now + idx * 0.1 + 0.16);
|
||
});
|
||
} else if (type === 'win') {
|
||
[523.25, 659.25, 783.99, 1046.50].forEach((freq, idx) => {
|
||
const osc = ctx.createOscillator();
|
||
const gain = ctx.createGain();
|
||
osc.type = 'triangle';
|
||
osc.frequency.setValueAtTime(freq, now + idx * 0.08);
|
||
gain.gain.setValueAtTime(0.15, now + idx * 0.08);
|
||
gain.gain.exponentialRampToValueAtTime(0.001, now + idx * 0.08 + 0.28);
|
||
osc.connect(gain);
|
||
gain.connect(ctx.destination);
|
||
osc.start(now + idx * 0.08);
|
||
osc.stop(now + idx * 0.08 + 0.28);
|
||
});
|
||
}
|
||
} catch (e) {
|
||
console.warn('Audio tone error:', e);
|
||
}
|
||
}
|
||
|
||
function toggleSound() {
|
||
soundEnabled = !soundEnabled;
|
||
localStorage.setItem('rhetorik_sound_enabled', soundEnabled);
|
||
updateSoundUI();
|
||
if (soundEnabled) playTone('flip');
|
||
}
|
||
|
||
function updateSoundUI() {
|
||
const icon = document.getElementById('soundIcon');
|
||
const label = document.getElementById('soundLabel');
|
||
if (soundEnabled) {
|
||
icon.className = 'fa-solid fa-volume-high text-indigo-600';
|
||
if (label) label.innerText = 'Ton An';
|
||
} else {
|
||
icon.className = 'fa-solid fa-volume-xmark text-slate-400';
|
||
if (label) label.innerText = 'Stumm';
|
||
}
|
||
}
|
||
|
||
// ==========================================
|
||
// FSRS-4.5 (FREE SPACED REPETITION SCHEDULER) ENGINE
|
||
// 17-Parameter DSR (Difficulty, Stability, Retrievability) Model
|
||
// ==========================================
|
||
const FSRS_WEIGHTS = [
|
||
0.4072, 1.1829, 3.1262, 15.4722, // w0-w3: Initial stability for ratings 1 (Again), 2 (Hard), 3 (Good), 4 (Easy)
|
||
7.2102, 0.5316, // w4, w5: Initial difficulty parameters
|
||
1.0651, 0.0234, // w6, w7: Next difficulty shift & mean reversion to D0(3)
|
||
1.616, 0.1544, 1.0824, // w8, w9, w10: Stability after recall
|
||
1.9813, 0.0953, 0.2975, 0.472, // w11, w12, w13, w14: Stability after forgetting
|
||
0.2655, 1.0 // w15: Hard penalty factor, w16: Easy bonus factor
|
||
];
|
||
const FSRS_FACTOR = 19 / 81; // ~0.2345679 (Power forgetting factor)
|
||
const FSRS_DESIRED_RETENTION = 0.90; // 90 % Behaltensquote
|
||
|
||
// Algorithm & Card States
|
||
let cardAlgo = localStorage.getItem('rhetorik_card_algo') || 'fsrs';
|
||
let fsrsCards = JSON.parse(localStorage.getItem('rhetorik_fsrs_cards') || '{}');
|
||
let cardLevels = JSON.parse(localStorage.getItem('rhetorik_leitner_levels') || '{}');
|
||
|
||
let activeCardCategory = 'highlighted';
|
||
let activeLeitnerBox = 'all';
|
||
let activeFsrsQueue = 'all';
|
||
let filteredCards = [...STILMITTEL_DATA];
|
||
|
||
let currentTabIndex = 'cards';
|
||
let currentCardIndex = 0;
|
||
let cardIsFlipped = false;
|
||
let cardFaceMode = 'term';
|
||
|
||
// Quiz State & Mistakes Tracking
|
||
let quizQuestions = [];
|
||
let currentQuizIndex = 0;
|
||
let quizScore = 0;
|
||
let quizStreak = 0;
|
||
let quizMaxStreak = 0;
|
||
let quizMistakes = JSON.parse(localStorage.getItem('rhetorik_quiz_mistakes') || '[]');
|
||
let isQuestionAnswered = false;
|
||
let currentQuizMode = 'highlighted';
|
||
|
||
let currentMatcherPairs = [];
|
||
|
||
// Glossary State
|
||
let currentGlossaryFilter = 'all';
|
||
|
||
// ==========================================
|
||
// FSRS MATH CALCULATIONS
|
||
// ==========================================
|
||
function getFsrsCard(id) {
|
||
return fsrsCards[id] || { s: 0, d: 0, reps: 0, lapses: 0, lastReview: 0, dueDate: 0, state: 0 };
|
||
}
|
||
|
||
function getElapsedDays(lastReview) {
|
||
if (!lastReview || lastReview <= 0) return 0;
|
||
return Math.max(0, (Date.now() - lastReview) / 86400000);
|
||
}
|
||
|
||
function calcRetrievability(tDays, s) {
|
||
if (!s || s <= 0) return 0;
|
||
return Math.pow(1 + FSRS_FACTOR * (tDays / s), -0.5);
|
||
}
|
||
|
||
function calcInitDifficulty(grade) {
|
||
const d = FSRS_WEIGHTS[4] - Math.exp(FSRS_WEIGHTS[5] * (grade - 1)) + 1;
|
||
return Math.min(10, Math.max(1, d));
|
||
}
|
||
|
||
function calcNextDifficulty(d, grade) {
|
||
const d0_3 = calcInitDifficulty(3);
|
||
const dPrime = d - FSRS_WEIGHTS[6] * (grade - 3);
|
||
const nextD = FSRS_WEIGHTS[7] * d0_3 + (1 - FSRS_WEIGHTS[7]) * dPrime;
|
||
return Math.min(10, Math.max(1, nextD));
|
||
}
|
||
|
||
function calcNextStability(cardData, grade, tDays) {
|
||
const w = FSRS_WEIGHTS;
|
||
if (!cardData || !cardData.reps || cardData.reps === 0) {
|
||
return Math.max(0.1, w[grade - 1]);
|
||
}
|
||
const r = Math.min(1, Math.max(0, calcRetrievability(tDays, cardData.s)));
|
||
const d = cardData.d || calcInitDifficulty(3);
|
||
|
||
if (grade === 1) {
|
||
const sForget = w[11] * Math.pow(d, -w[12]) * (Math.pow(cardData.s + 1, w[13]) - 1) * Math.exp((1 - r) * w[14]);
|
||
return Math.max(0.1, Math.min(cardData.s, sForget));
|
||
} else {
|
||
let h = 1;
|
||
if (grade === 2) h = w[15];
|
||
if (grade === 4) h = w[16];
|
||
const sRecall = cardData.s * (1 + Math.exp(w[8]) * (11 - d) * Math.pow(cardData.s, -w[9]) * (Math.exp((1 - r) * w[10]) - 1) * h);
|
||
return Math.max(0.1, sRecall);
|
||
}
|
||
}
|
||
|
||
function calcNextInterval(s, grade) {
|
||
if (grade === 1) return 0.007; // ca. 10 Minuten
|
||
if (grade === 2) return Math.max(1, Math.round(s * 0.85));
|
||
if (grade === 3) return Math.max(1, Math.round(s));
|
||
if (grade === 4) return Math.max(Math.round(s * 1.3), Math.round(s) + 1);
|
||
return 1;
|
||
}
|
||
|
||
function formatIntervalText(days) {
|
||
if (days <= 0.02) return '< 10 Min';
|
||
if (days < 1) return 'heute';
|
||
const roundDays = Math.round(days);
|
||
if (roundDays === 1) return '1 Tag';
|
||
if (roundDays < 30) return `${roundDays} Tage`;
|
||
if (roundDays < 365) {
|
||
const months = (days / 30).toFixed(days % 30 === 0 ? 0 : 1);
|
||
return `${months} Mon.`;
|
||
}
|
||
return `${(days / 365).toFixed(1)} J.`;
|
||
}
|
||
|
||
function isFsrsDue(cardData) {
|
||
if (!cardData || !cardData.reps || cardData.reps === 0) return true;
|
||
if (cardData.dueDate && cardData.dueDate <= Date.now()) return true;
|
||
const t = getElapsedDays(cardData.lastReview);
|
||
const r = calcRetrievability(t, cardData.s);
|
||
return r < FSRS_DESIRED_RETENTION;
|
||
}
|
||
|
||
// ==========================================
|
||
// LEITNER 5-BOX HELPERS
|
||
// ==========================================
|
||
function getItemLevel(id) {
|
||
return cardLevels[id] || 1;
|
||
}
|
||
|
||
function setItemLevel(id, level) {
|
||
cardLevels[id] = Math.max(1, Math.min(5, level));
|
||
localStorage.setItem('rhetorik_leitner_levels', JSON.stringify(cardLevels));
|
||
updateMasteredHeader();
|
||
}
|
||
|
||
function updateMasteredHeader() {
|
||
const isFsrs = cardAlgo === 'fsrs';
|
||
const headerStat = document.getElementById('globalMasteredStat');
|
||
|
||
if (isFsrs) {
|
||
const fsrsItems = Object.values(fsrsCards);
|
||
const stableCount = fsrsItems.filter(c => c && c.reps > 0 && c.s >= 14).length;
|
||
const dueCount = STILMITTEL_DATA.filter(item => isFsrsDue(fsrsCards[item.id])).length;
|
||
if (headerStat) {
|
||
headerStat.innerText = `${stableCount} / ${STILMITTEL_DATA.length} stabil (${dueCount} fällig)`;
|
||
}
|
||
} else {
|
||
const masteredCount = STILMITTEL_DATA.filter(item => (cardLevels[item.id] || 1) >= 5).length;
|
||
if (headerStat) {
|
||
headerStat.innerText = `${masteredCount} / ${STILMITTEL_DATA.length} gelernt`;
|
||
}
|
||
}
|
||
|
||
updateQuizMistakesBadge();
|
||
updateQueueFilterOptions();
|
||
}
|
||
|
||
function updateQueueFilterOptions() {
|
||
const qSelect = document.getElementById('fsrsQueueFilter');
|
||
if (!qSelect) return;
|
||
const dueCount = STILMITTEL_DATA.filter(item => isFsrsDue(fsrsCards[item.id])).length;
|
||
const learningCount = STILMITTEL_DATA.filter(item => fsrsCards[item.id] && fsrsCards[item.id].reps > 0).length;
|
||
const newCount = STILMITTEL_DATA.filter(item => !fsrsCards[item.id] || !fsrsCards[item.id].reps).length;
|
||
|
||
const optDue = qSelect.querySelector('option[value="due"]');
|
||
const optLearning = qSelect.querySelector('option[value="learning"]');
|
||
const optNew = qSelect.querySelector('option[value="new"]');
|
||
|
||
if (optDue) optDue.innerText = `📅 Jetzt fällig (${dueCount})`;
|
||
if (optLearning) optLearning.innerText = `📈 In Wiederholung (${learningCount})`;
|
||
if (optNew) optNew.innerText = `🌱 Noch unberührt (${newCount})`;
|
||
}
|
||
|
||
function updateQuizMistakesBadge() {
|
||
const badge = document.getElementById('quizMistakesBadge');
|
||
const countEl = document.getElementById('quizMistakesCount');
|
||
if (badge && countEl) {
|
||
if (quizMistakes.length > 0) {
|
||
badge.classList.remove('hidden');
|
||
countEl.innerText = quizMistakes.length;
|
||
} else {
|
||
badge.classList.add('hidden');
|
||
}
|
||
}
|
||
const weakBtnCount = document.getElementById('quizWeakBtnCount');
|
||
if (weakBtnCount) {
|
||
weakBtnCount.innerText = quizMistakes.length;
|
||
}
|
||
}
|
||
|
||
function resetCardProgressPrompt() {
|
||
if (cardAlgo === 'fsrs') {
|
||
if (confirm('Möchtest du den FSRS-Lernverlauf (Stabilität, Schwierigkeit & Fälligkeiten) für alle Karten zurücksetzen?')) {
|
||
fsrsCards = {};
|
||
localStorage.removeItem('rhetorik_fsrs_cards');
|
||
applyCardFilters();
|
||
updateMasteredHeader();
|
||
showToast('FSRS-Lernverlauf zurückgesetzt!', 'fa-rotate-left');
|
||
}
|
||
} else {
|
||
resetLeitnerProgress();
|
||
}
|
||
}
|
||
|
||
function resetLeitnerProgress() {
|
||
if (confirm('Möchtest du den Leitner-Lernfortschritt für alle Karteikarten wirklich auf Kasten 1 zurücksetzen?')) {
|
||
cardLevels = {};
|
||
localStorage.removeItem('rhetorik_leitner_levels');
|
||
applyCardFilters();
|
||
updateMasteredHeader();
|
||
showToast('Leitner-Lernstand zurückgesetzt!', 'fa-rotate-left');
|
||
}
|
||
}
|
||
|
||
function setSpacedRepetitionAlgo(algo) {
|
||
cardAlgo = algo;
|
||
localStorage.setItem('rhetorik_card_algo', cardAlgo);
|
||
|
||
const isFsrs = cardAlgo === 'fsrs';
|
||
const fsrsBtn = document.getElementById('algoFsrsBtn');
|
||
const leitnerBtn = document.getElementById('algoLeitnerBtn');
|
||
const title = document.getElementById('cardSectionTitle');
|
||
const badge = document.getElementById('algoHeaderBadge');
|
||
const subtitle = document.getElementById('cardSectionSubtitle');
|
||
|
||
const fsrsFilterWrap = document.getElementById('fsrsFilterWrap');
|
||
const leitnerFilterWrap = document.getElementById('leitnerFilterWrap');
|
||
const fsrsMetrics = document.getElementById('fsrsMetricsContainer');
|
||
const leitnerMetrics = document.getElementById('leitnerMetricsContainer');
|
||
const fsrsActions = document.getElementById('fsrsActionButtons');
|
||
const leitnerActions = document.getElementById('leitnerActionButtons');
|
||
|
||
if (isFsrs) {
|
||
if (fsrsBtn) fsrsBtn.className = 'px-2.5 sm:px-3 py-1.5 rounded-lg bg-white text-indigo-700 shadow-sm transition font-bold flex items-center gap-1.5';
|
||
if (leitnerBtn) leitnerBtn.className = 'px-2.5 sm:px-3 py-1.5 rounded-lg text-slate-600 hover:text-slate-900 transition font-semibold flex items-center gap-1.5';
|
||
if (title) title.innerHTML = '<i class="fa-solid fa-brain text-indigo-600"></i><span>FSRS-4.5 Karteikasten</span>';
|
||
if (badge) badge.innerText = 'Spaced Repetition';
|
||
if (subtitle) subtitle.innerText = 'Modernes 17-Parameter DSR-Modell (Retrievability, Stability, Difficulty) – 20–30 % weniger Wiederholungen bei 90 % Behaltungsquote.';
|
||
if (fsrsFilterWrap) fsrsFilterWrap.classList.remove('hidden');
|
||
if (leitnerFilterWrap) leitnerFilterWrap.classList.add('hidden');
|
||
if (fsrsMetrics) fsrsMetrics.classList.remove('hidden');
|
||
if (leitnerMetrics) leitnerMetrics.classList.add('hidden');
|
||
if (fsrsActions) fsrsActions.classList.remove('hidden');
|
||
if (leitnerActions) leitnerActions.classList.add('hidden');
|
||
} else {
|
||
if (fsrsBtn) fsrsBtn.className = 'px-2.5 sm:px-3 py-1.5 rounded-lg text-slate-600 hover:text-slate-900 transition font-semibold flex items-center gap-1.5';
|
||
if (leitnerBtn) leitnerBtn.className = 'px-2.5 sm:px-3 py-1.5 rounded-lg bg-white text-indigo-700 shadow-sm transition font-bold flex items-center gap-1.5';
|
||
if (title) title.innerHTML = '<i class="fa-solid fa-boxes-stacked text-indigo-600"></i><span>Leitner-Karteikarten (5-Kasten)</span>';
|
||
if (badge) badge.innerText = 'Klassisch';
|
||
if (subtitle) subtitle.innerText = 'Klassisches 5-Kasten-Lernsystem: Wische nach links (wiederholen) oder rechts (gekonnt).';
|
||
if (fsrsFilterWrap) fsrsFilterWrap.classList.add('hidden');
|
||
if (leitnerFilterWrap) leitnerFilterWrap.classList.remove('hidden');
|
||
if (fsrsMetrics) fsrsMetrics.classList.add('hidden');
|
||
if (leitnerMetrics) leitnerMetrics.classList.remove('hidden');
|
||
if (fsrsActions) fsrsActions.classList.add('hidden');
|
||
if (leitnerActions) leitnerActions.classList.remove('hidden');
|
||
}
|
||
|
||
applyCardFilters();
|
||
updateMasteredHeader();
|
||
}
|
||
|
||
function filterByFsrsQueue(queue) {
|
||
activeFsrsQueue = queue;
|
||
applyCardFilters();
|
||
}
|
||
|
||
function filterByLeitnerBox(box) {
|
||
activeLeitnerBox = box;
|
||
applyCardFilters();
|
||
}
|
||
|
||
function applyCardFilters() {
|
||
const isFsrs = cardAlgo === 'fsrs';
|
||
|
||
filteredCards = STILMITTEL_DATA.filter(item => {
|
||
if (activeCardCategory === 'highlighted') {
|
||
if (!item.highlighted) return false;
|
||
} else if (activeCardCategory === 'zusatz') {
|
||
if (item.highlighted) return false;
|
||
} else if (activeCardCategory !== 'all' && item.category !== activeCardCategory) {
|
||
return false;
|
||
}
|
||
|
||
if (isFsrs) {
|
||
const cardData = fsrsCards[item.id];
|
||
if (activeFsrsQueue === 'due') {
|
||
return isFsrsDue(cardData);
|
||
} else if (activeFsrsQueue === 'learning') {
|
||
return cardData && cardData.reps > 0;
|
||
} else if (activeFsrsQueue === 'new') {
|
||
return !cardData || !cardData.reps || cardData.reps === 0;
|
||
}
|
||
return true;
|
||
} else {
|
||
if (activeLeitnerBox !== 'all') {
|
||
const lvl = getItemLevel(item.id);
|
||
if (lvl.toString() !== activeLeitnerBox.toString()) return false;
|
||
}
|
||
return true;
|
||
}
|
||
});
|
||
|
||
if (filteredCards.length === 0) {
|
||
if (isFsrs && activeFsrsQueue === 'due') {
|
||
showToast('Großartig! Alle fälligen Karten wurden wiederholt. Zeige alle.', 'fa-sparkles');
|
||
activeFsrsQueue = 'all';
|
||
const qSelect = document.getElementById('fsrsQueueFilter');
|
||
if (qSelect) qSelect.value = 'all';
|
||
return applyCardFilters();
|
||
}
|
||
filteredCards = [...STILMITTEL_DATA];
|
||
}
|
||
|
||
currentCardIndex = 0;
|
||
renderCurrentCard();
|
||
}
|
||
|
||
function setCardCategoryFilter(cat) {
|
||
activeCardCategory = cat;
|
||
document.querySelectorAll('.card-cat-pill').forEach(btn => {
|
||
btn.className = 'card-cat-pill px-2.5 py-1 rounded-lg font-semibold bg-slate-100 text-slate-600 hover:bg-slate-200';
|
||
});
|
||
const activeBtn = document.getElementById(`cardCatBtn-${cat}`);
|
||
if (activeBtn) {
|
||
activeBtn.className = 'card-cat-pill px-2.5 py-1 rounded-lg font-bold bg-indigo-50 text-indigo-700 border border-indigo-200';
|
||
}
|
||
applyCardFilters();
|
||
}
|
||
|
||
function renderCurrentCard() {
|
||
if (filteredCards.length === 0) return;
|
||
if (currentCardIndex >= filteredCards.length) currentCardIndex = 0;
|
||
|
||
const item = filteredCards[currentCardIndex];
|
||
const isFsrs = cardAlgo === 'fsrs';
|
||
|
||
cardIsFlipped = false;
|
||
const cardEl = document.getElementById('flashcardElement');
|
||
cardEl.classList.remove('rotate-y-180', 'swipe-left', 'swipe-right');
|
||
|
||
const progressPercent = ((currentCardIndex + 1) / filteredCards.length) * 100;
|
||
document.getElementById('cardProgressBar').style.width = `${progressPercent}%`;
|
||
document.getElementById('cardCounter').innerText = `Karte ${currentCardIndex + 1} von ${filteredCards.length}`;
|
||
document.getElementById('cardProgressLabel').innerText = `Karte ${currentCardIndex + 1} von ${filteredCards.length} (${item.categoryLabel})`;
|
||
|
||
if (isFsrs) {
|
||
const cardData = getFsrsCard(item.id);
|
||
const t = getElapsedDays(cardData.lastReview);
|
||
const rVal = cardData.reps > 0 ? Math.round(calcRetrievability(t, cardData.s) * 100) : 100;
|
||
const sVal = cardData.s ? (cardData.s >= 10 ? cardData.s.toFixed(0) : cardData.s.toFixed(1)) : '0';
|
||
const dVal = cardData.d ? cardData.d.toFixed(1) : '-';
|
||
const due = isFsrsDue(cardData);
|
||
|
||
// Update DSR Badges
|
||
const rBadge = document.getElementById('fsrsRetrievabilityBadge');
|
||
if (rBadge) {
|
||
rBadge.innerHTML = `<i class="fa-solid fa-gauge-high text-emerald-600"></i><span>R: ${rVal}%</span>`;
|
||
if (rVal >= 90) {
|
||
rBadge.className = 'px-2.5 py-0.5 rounded-lg bg-emerald-50 text-emerald-800 border border-emerald-200 font-extrabold flex items-center gap-1';
|
||
} else if (rVal >= 75) {
|
||
rBadge.className = 'px-2.5 py-0.5 rounded-lg bg-amber-50 text-amber-800 border border-amber-200 font-extrabold flex items-center gap-1';
|
||
} else {
|
||
rBadge.className = 'px-2.5 py-0.5 rounded-lg bg-rose-50 text-rose-800 border border-rose-200 font-extrabold flex items-center gap-1';
|
||
}
|
||
}
|
||
|
||
const sBadge = document.getElementById('fsrsStabilityBadge');
|
||
if (sBadge) {
|
||
sBadge.innerHTML = `<i class="fa-solid fa-calendar-day text-blue-500"></i><span>S: ${sVal} T</span>`;
|
||
}
|
||
|
||
const dBadge = document.getElementById('fsrsDifficultyBadge');
|
||
if (dBadge) {
|
||
dBadge.innerHTML = `<i class="fa-solid fa-weight-scale text-amber-500"></i><span>D: ${dVal}</span>`;
|
||
}
|
||
|
||
const dueBadge = document.getElementById('fsrsDueBadge');
|
||
if (dueBadge) {
|
||
if (!cardData.reps || cardData.reps === 0) {
|
||
dueBadge.className = 'px-2.5 py-0.5 rounded-lg bg-indigo-50 text-indigo-700 border border-indigo-200 font-extrabold flex items-center gap-1';
|
||
dueBadge.innerHTML = '<i class="fa-solid fa-seedling text-indigo-500"></i><span>Neu</span>';
|
||
} else if (due) {
|
||
dueBadge.className = 'px-2.5 py-0.5 rounded-lg bg-rose-50 text-rose-700 border border-rose-200 font-extrabold flex items-center gap-1';
|
||
dueBadge.innerHTML = '<i class="fa-solid fa-clock text-rose-500"></i><span>Fällig</span>';
|
||
} else {
|
||
dueBadge.className = 'px-2.5 py-0.5 rounded-lg bg-emerald-50 text-emerald-700 border border-emerald-200 font-extrabold flex items-center gap-1';
|
||
dueBadge.innerHTML = '<i class="fa-solid fa-check text-emerald-500"></i><span>Bereit</span>';
|
||
}
|
||
}
|
||
|
||
// Live calculation of next predicted intervals for all 4 ratings
|
||
const nextS1 = calcNextStability(cardData, 1, t);
|
||
const nextS2 = calcNextStability(cardData, 2, t);
|
||
const nextS3 = calcNextStability(cardData, 3, t);
|
||
const nextS4 = calcNextStability(cardData, 4, t);
|
||
|
||
const i1 = calcNextInterval(nextS1, 1);
|
||
const i2 = calcNextInterval(nextS2, 2);
|
||
const i3 = calcNextInterval(nextS3, 3);
|
||
const i4 = calcNextInterval(nextS4, 4);
|
||
|
||
const lbl1 = document.getElementById('fsrsIntervalAgain');
|
||
const lbl2 = document.getElementById('fsrsIntervalHard');
|
||
const lbl3 = document.getElementById('fsrsIntervalGood');
|
||
const lbl4 = document.getElementById('fsrsIntervalEasy');
|
||
|
||
if (lbl1) lbl1.innerText = formatIntervalText(i1);
|
||
if (lbl2) lbl2.innerText = `+${formatIntervalText(i2)}`;
|
||
if (lbl3) lbl3.innerText = `+${formatIntervalText(i3)}`;
|
||
if (lbl4) lbl4.innerText = `+${formatIntervalText(i4)}`;
|
||
} else {
|
||
const level = getItemLevel(item.id);
|
||
const boxNames = ['', 'Kasten 1 (Neu)', 'Kasten 2 (Aufbau)', 'Kasten 3 (Vertieft)', 'Kasten 4 (Fast sicher)', 'Kasten 5 (Gemeistert)'];
|
||
document.getElementById('cardBoxBadge').innerText = boxNames[level] || `Kasten ${level}`;
|
||
|
||
const dotsHtml = [1, 2, 3, 4, 5].map(i => {
|
||
const bg = i <= level ? 'bg-indigo-600' : 'bg-slate-200';
|
||
return `<span class="w-2.5 h-2.5 rounded-full ${bg} transition-colors"></span>`;
|
||
}).join('');
|
||
document.getElementById('leitnerBoxesVisual').innerHTML = dotsHtml;
|
||
}
|
||
|
||
document.getElementById('cardBadgeFront').innerText = item.categoryLabel;
|
||
document.getElementById('cardBackCategory').innerText = item.categoryLabel;
|
||
|
||
if (cardFaceMode === 'term') {
|
||
document.getElementById('cardFrontHint').innerText = 'Rhetorisches Stilmittel';
|
||
document.getElementById('cardFrontMain').innerText = item.term;
|
||
document.getElementById('cardFrontSub').innerText = 'Tippe zum Aufdecken der Definition & Beispiele';
|
||
} else {
|
||
document.getElementById('cardFrontHint').innerText = 'Beispiel & Signal';
|
||
document.getElementById('cardFrontMain').innerText = `„${item.examples[0]}“`;
|
||
document.getElementById('cardFrontSub').innerText = 'Welches Stilmittel liegt hier vor?';
|
||
}
|
||
|
||
document.getElementById('cardBackTerm').querySelector('span').innerText = item.term;
|
||
document.getElementById('cardBackDefinition').innerText = item.definition;
|
||
document.getElementById('cardBackExamples').innerText = item.examples.map(e => `„${e}“`).join(' • ');
|
||
|
||
const tipEl = document.getElementById('cardBackTip');
|
||
if (item.tip) {
|
||
document.getElementById('cardBackTipContainer').classList.remove('hidden');
|
||
tipEl.innerText = item.tip;
|
||
} else {
|
||
document.getElementById('cardBackTipContainer').classList.add('hidden');
|
||
}
|
||
|
||
const hlBadge = document.getElementById('cardHighlightBadge');
|
||
if (item.highlighted) {
|
||
hlBadge.classList.remove('hidden');
|
||
hlBadge.innerText = '🌟 Kernstoff';
|
||
hlBadge.className = 'text-[11px] font-bold uppercase px-2.5 py-0.5 rounded-full bg-indigo-500/30 text-indigo-200 border border-indigo-400/40';
|
||
} else {
|
||
hlBadge.classList.remove('hidden');
|
||
hlBadge.innerText = '🔍 Zusatzinfo';
|
||
hlBadge.className = 'text-[11px] font-bold uppercase px-2.5 py-0.5 rounded-full bg-slate-700/50 text-slate-300 border border-slate-600/50';
|
||
}
|
||
}
|
||
|
||
function flipCard() {
|
||
cardIsFlipped = !cardIsFlipped;
|
||
const cardEl = document.getElementById('flashcardElement');
|
||
if (cardIsFlipped) {
|
||
cardEl.classList.add('rotate-y-180');
|
||
} else {
|
||
cardEl.classList.remove('rotate-y-180');
|
||
}
|
||
playTone('flip');
|
||
}
|
||
|
||
function rateFsrsCard(grade) {
|
||
if (filteredCards.length === 0) return;
|
||
const item = filteredCards[currentCardIndex];
|
||
const cardData = getFsrsCard(item.id);
|
||
const t = getElapsedDays(cardData.lastReview);
|
||
|
||
const nextS = calcNextStability(cardData, grade, t);
|
||
const nextD = (!cardData.reps || cardData.reps === 0) ? calcInitDifficulty(grade) : calcNextDifficulty(cardData.d, grade);
|
||
const intervalDays = calcNextInterval(nextS, grade);
|
||
const now = Date.now();
|
||
const nextDueDate = now + Math.round(intervalDays * 86400000);
|
||
|
||
const updatedCard = {
|
||
s: parseFloat(nextS.toFixed(4)),
|
||
d: parseFloat(nextD.toFixed(4)),
|
||
reps: (cardData.reps || 0) + 1,
|
||
lapses: grade === 1 ? (cardData.lapses || 0) + 1 : (cardData.lapses || 0),
|
||
lastReview: now,
|
||
dueDate: nextDueDate,
|
||
state: grade === 1 ? 3 : 2
|
||
};
|
||
|
||
fsrsCards[item.id] = updatedCard;
|
||
localStorage.setItem('rhetorik_fsrs_cards', JSON.stringify(fsrsCards));
|
||
|
||
const cardEl = document.getElementById('flashcardElement');
|
||
if (grade === 1) {
|
||
playTone('error');
|
||
addXP(5, 'FSRS: Noch üben');
|
||
cardEl.classList.add('swipe-left');
|
||
showToast(`„${item.term}“: Wiederholung ${formatIntervalText(intervalDays)}`, 'fa-rotate-left');
|
||
} else if (grade === 2) {
|
||
playTone('flip');
|
||
addXP(10, 'FSRS: Schwer');
|
||
showToast(`„${item.term}“: Wiederholung in ${formatIntervalText(intervalDays)}`, 'fa-clock');
|
||
} else if (grade === 3) {
|
||
playTone('success');
|
||
addXP(15, 'FSRS: Gut');
|
||
cardEl.classList.add('swipe-right');
|
||
showToast(`„${item.term}“: Wiederholung in ${formatIntervalText(intervalDays)}`, 'fa-check');
|
||
} else if (grade === 4) {
|
||
playTone('win');
|
||
addXP(20, 'FSRS: Perfekt!');
|
||
cardEl.classList.add('swipe-right');
|
||
showToast(`„${item.term}“: Wiederholung in ${formatIntervalText(intervalDays)}`, 'fa-wand-magic-sparkles');
|
||
}
|
||
|
||
updateMasteredHeader();
|
||
|
||
setTimeout(() => {
|
||
nextCard();
|
||
}, 250);
|
||
}
|
||
|
||
function markCardMastered(isMastered) {
|
||
if (filteredCards.length === 0) return;
|
||
const item = filteredCards[currentCardIndex];
|
||
const currentLvl = getItemLevel(item.id);
|
||
const cardEl = document.getElementById('flashcardElement');
|
||
|
||
if (isMastered) {
|
||
setItemLevel(item.id, currentLvl + 1);
|
||
playTone('success');
|
||
addXP(20, 'Karte gemeistert');
|
||
cardEl.classList.add('swipe-right');
|
||
showToast(`„${item.term}“ aufgestiegen in Kasten ${Math.min(5, currentLvl + 1)}!`, 'fa-circle-up');
|
||
} else {
|
||
setItemLevel(item.id, Math.max(1, currentLvl - 1));
|
||
playTone('error');
|
||
cardEl.classList.add('swipe-left');
|
||
showToast(`„${item.term}“ wiederholt in Kasten ${Math.max(1, currentLvl - 1)}`, 'fa-rotate-left');
|
||
}
|
||
|
||
setTimeout(() => {
|
||
nextCard();
|
||
}, 250);
|
||
}
|
||
|
||
function nextCard() {
|
||
if (filteredCards.length === 0) return;
|
||
currentCardIndex = (currentCardIndex + 1) % filteredCards.length;
|
||
renderCurrentCard();
|
||
}
|
||
|
||
function prevCard() {
|
||
if (filteredCards.length === 0) return;
|
||
currentCardIndex = (currentCardIndex - 1 + filteredCards.length) % filteredCards.length;
|
||
renderCurrentCard();
|
||
}
|
||
|
||
function shuffleCards() {
|
||
for (let i = filteredCards.length - 1; i > 0; i--) {
|
||
const j = Math.floor(Math.random() * (i + 1));
|
||
[filteredCards[i], filteredCards[j]] = [filteredCards[j], filteredCards[i]];
|
||
}
|
||
currentCardIndex = 0;
|
||
renderCurrentCard();
|
||
playTone('flip');
|
||
showToast('Karten gemischt!', 'fa-shuffle');
|
||
}
|
||
|
||
function setCardFaceMode(mode) {
|
||
cardFaceMode = mode;
|
||
document.getElementById('cardModeTermBtn').className = mode === 'term'
|
||
? 'px-2.5 sm:px-3 py-1.5 rounded-lg bg-white text-indigo-700 shadow-sm transition font-bold'
|
||
: 'px-2.5 sm:px-3 py-1.5 rounded-lg text-slate-600 hover:text-slate-900 transition font-semibold';
|
||
document.getElementById('cardModeExampleBtn').className = mode === 'example'
|
||
? 'px-2.5 sm:px-3 py-1.5 rounded-lg bg-white text-indigo-700 shadow-sm transition font-bold'
|
||
: 'px-2.5 sm:px-3 py-1.5 rounded-lg text-slate-600 hover:text-slate-900 transition font-semibold';
|
||
renderCurrentCard();
|
||
}
|
||
|
||
// Touch Swipe Gestures (supports both FSRS and Leitner)
|
||
let touchStartX = 0;
|
||
let touchStartY = 0;
|
||
const cardTouchContainer = document.getElementById('flashcardContainer');
|
||
|
||
cardTouchContainer.addEventListener('touchstart', (e) => {
|
||
touchStartX = e.changedTouches[0].screenX;
|
||
touchStartY = e.changedTouches[0].screenY;
|
||
}, { passive: true });
|
||
|
||
cardTouchContainer.addEventListener('touchend', (e) => {
|
||
const touchEndX = e.changedTouches[0].screenX;
|
||
const touchEndY = e.changedTouches[0].screenY;
|
||
const diffX = touchEndX - touchStartX;
|
||
const diffY = touchEndY - touchStartY;
|
||
|
||
if (Math.abs(diffX) > 50 && Math.abs(diffX) > Math.abs(diffY)) {
|
||
if (cardAlgo === 'fsrs') {
|
||
if (diffX > 0) {
|
||
rateFsrsCard(3); // Gut
|
||
} else {
|
||
rateFsrsCard(1); // Nochmal
|
||
}
|
||
} else {
|
||
if (diffX > 0) {
|
||
markCardMastered(true);
|
||
} else {
|
||
markCardMastered(false);
|
||
}
|
||
}
|
||
} else if (Math.abs(diffX) < 15 && Math.abs(diffY) < 15) {
|
||
flipCard();
|
||
}
|
||
}, { passive: true });
|
||
|
||
// ==========================================
|
||
// AUDIO & WEB SPEECH SYNTHESIS
|
||
// ==========================================
|
||
function speakText(text, event) {
|
||
if (event) event.stopPropagation();
|
||
if (!('speechSynthesis' in window)) {
|
||
showToast('Sprachausgabe im Browser nicht verfügbar', 'fa-volume-xmark');
|
||
return;
|
||
}
|
||
try {
|
||
window.speechSynthesis.cancel();
|
||
// Remove surrounding quotes and context in brackets for fluid natural speech
|
||
let clean = text.replace(/„|“|"|'/g, '').replace(/\(.*?\)/g, '').trim();
|
||
if (!clean) return;
|
||
const utterance = new SpeechSynthesisUtterance(clean);
|
||
utterance.lang = 'de-DE';
|
||
utterance.rate = 0.95;
|
||
utterance.pitch = 1.0;
|
||
window.speechSynthesis.speak(utterance);
|
||
|
||
audioListenCount++;
|
||
localStorage.setItem('rhetorik_audio_count', audioListenCount.toString());
|
||
addXP(5);
|
||
} catch (err) {
|
||
console.warn('SpeechSynthesis error:', err);
|
||
}
|
||
}
|
||
|
||
function speakCurrentQuizPhrase(event) {
|
||
if (currentQuizIndex < quizQuestions.length) {
|
||
const q = quizQuestions[currentQuizIndex];
|
||
speakText(q.example, event);
|
||
}
|
||
}
|
||
|
||
function speakCurrentCard(event) {
|
||
if (filteredCards.length > 0 && currentCardIndex < filteredCards.length) {
|
||
const item = filteredCards[currentCardIndex];
|
||
if (cardFaceMode === 'term' && !cardIsFlipped) {
|
||
speakText(item.term, event);
|
||
} else {
|
||
speakText(item.examples[0], event);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ==========================================
|
||
// FUZZY MATCHER & LEVENSHTEIN DISTANCE
|
||
// ==========================================
|
||
function levenshteinDistance(s1, s2) {
|
||
if (!s1) return s2 ? s2.length : 0;
|
||
if (!s2) return s1.length;
|
||
if (s1.length < s2.length) return levenshteinDistance(s2, s1);
|
||
|
||
let prevRow = Array.from({ length: s2.length + 1 }, (_, i) => i);
|
||
for (let i = 0; i < s1.length; i++) {
|
||
let currRow = [i + 1];
|
||
for (let j = 0; j < s2.length; j++) {
|
||
let cost = s1[i] === s2[j] ? 0 : 1;
|
||
currRow.push(Math.min(
|
||
currRow[j] + 1, // Insertion
|
||
prevRow[j + 1] + 1, // Deletion
|
||
prevRow[j] + cost // Substitution
|
||
));
|
||
}
|
||
prevRow = currRow;
|
||
}
|
||
return prevRow[s2.length];
|
||
}
|
||
|
||
function checkFuzzyMatch(userInput, targetTerm) {
|
||
if (!userInput) return { isMatch: false, exact: false, dist: 99 };
|
||
const normUser = userInput.toLowerCase().replace(/[^a-zäöüß0-9]/g, '');
|
||
const normTarget = targetTerm.toLowerCase().replace(/[^a-zäöüß0-9]/g, '');
|
||
|
||
if (normUser === normTarget) {
|
||
return { isMatch: true, exact: true, dist: 0 };
|
||
}
|
||
|
||
// Special aliases and dual terms
|
||
if (normTarget.includes('tautologie') || normTarget.includes('pleonasmus')) {
|
||
if (normUser === 'tautologie' || normUser === 'pleonasmus') {
|
||
return { isMatch: true, exact: true, dist: 0 };
|
||
}
|
||
}
|
||
if (normTarget.includes('parsprototo')) {
|
||
if (normUser === 'parsprototo' || normUser === 'synekdoche' || normUser === 'partsprototo') {
|
||
return { isMatch: true, exact: true, dist: 0 };
|
||
}
|
||
}
|
||
|
||
const dist = levenshteinDistance(normUser, normTarget);
|
||
const maxAllowed = normTarget.length > 7 ? 2 : (normTarget.length > 4 ? 1 : 0);
|
||
|
||
if (dist <= maxAllowed) {
|
||
return { isMatch: true, exact: false, dist: dist };
|
||
}
|
||
return { isMatch: false, exact: false, dist: dist };
|
||
}
|
||
|
||
// ==========================================
|
||
// NOTENSCHLÜSSEL & EXAM GRADING
|
||
// ==========================================
|
||
function calculateExamGrade(score, total) {
|
||
const percent = Math.round((score / total) * 100);
|
||
if (percent >= 90) {
|
||
return {
|
||
grade: 1,
|
||
name: "Sehr gut (1)",
|
||
points: "15 - 13 Notenpunkte",
|
||
badgeClass: "bg-emerald-100 text-emerald-900 border-emerald-300",
|
||
icon: "fa-crown",
|
||
iconColor: "text-amber-500",
|
||
feedback: "Ausgezeichnete Leistung! Du beherrschst die rhetorischen Stilmittel absolut souverän und bist bestens für jede Textanalyse gerüstet."
|
||
};
|
||
} else if (percent >= 80) {
|
||
return {
|
||
grade: 2,
|
||
name: "Gut (2)",
|
||
points: "12 - 10 Notenpunkte",
|
||
badgeClass: "bg-blue-100 text-blue-900 border-blue-300",
|
||
icon: "fa-star",
|
||
iconColor: "text-blue-500",
|
||
feedback: "Sehr gute Textanalyse-Kenntnisse mit nur minimalen Unsicherheiten. Wiederhole kurz die Verwechslungsfallen."
|
||
};
|
||
} else if (percent >= 65) {
|
||
return {
|
||
grade: 3,
|
||
name: "Befriedigend (3)",
|
||
points: "9 - 7 Notenpunkte",
|
||
badgeClass: "bg-amber-100 text-amber-900 border-amber-300",
|
||
icon: "fa-thumbs-up",
|
||
iconColor: "text-amber-600",
|
||
feedback: "Solides Grundlagenwissen. Ein gezieltes Schwachstellen-Training bringt dich sicher in den Bereich 'Gut'."
|
||
};
|
||
} else if (percent >= 50) {
|
||
return {
|
||
grade: 4,
|
||
name: "Ausreichend (4)",
|
||
points: "6 - 5 Notenpunkte",
|
||
badgeClass: "bg-orange-100 text-orange-900 border-orange-300",
|
||
icon: "fa-triangle-exclamation",
|
||
iconColor: "text-orange-500",
|
||
feedback: "Bestanden, aber mit spürbaren Lücken. Nutze die Karteikarten und das Glossar, um typische Fallen zu vermeiden."
|
||
};
|
||
} else if (percent >= 30) {
|
||
return {
|
||
grade: 5,
|
||
name: "Mangelhaft (5)",
|
||
points: "4 - 1 Notenpunkte",
|
||
badgeClass: "bg-rose-100 text-rose-900 border-rose-300",
|
||
icon: "fa-circle-xmark",
|
||
iconColor: "text-rose-500",
|
||
feedback: "Die Grundlagen sitzen noch nicht fest genug. Wiederhole systematisch den Kernstoff mit den Karteikarten."
|
||
};
|
||
} else {
|
||
return {
|
||
grade: 6,
|
||
name: "Ungenügend (6)",
|
||
points: "0 Notenpunkte",
|
||
badgeClass: "bg-slate-200 text-slate-900 border-slate-300",
|
||
icon: "fa-skull-crossbones",
|
||
iconColor: "text-slate-600",
|
||
feedback: "Nicht bestanden. Keine Panik: Starte am besten im Glossar und arbeite dich durch die 17 Kernstoff-Figuren."
|
||
};
|
||
}
|
||
}
|
||
|
||
// ==========================================
|
||
// QUIZ ENGINE (CHOICE, FREITEXT, EXAM & WEAKSPOTS)
|
||
// ==========================================
|
||
let currentQuizInputType = 'choice'; // 'choice' | 'text'
|
||
let examTimeRemaining = 600; // 10 minutes in seconds
|
||
let examTimerInterval = null;
|
||
let examStartTime = null;
|
||
|
||
function setQuizInputType(type) {
|
||
currentQuizInputType = type;
|
||
const choiceBtn = document.getElementById('quizInputChoiceBtn');
|
||
const textBtn = document.getElementById('quizInputTextBtn');
|
||
|
||
if (type === 'choice') {
|
||
choiceBtn.className = 'px-2.5 py-1.5 rounded-lg bg-white text-indigo-700 shadow-sm font-bold transition flex items-center gap-1';
|
||
textBtn.className = 'px-2.5 py-1.5 rounded-lg text-slate-600 hover:text-slate-900 transition font-semibold flex items-center gap-1';
|
||
} else {
|
||
textBtn.className = 'px-2.5 py-1.5 rounded-lg bg-white text-indigo-700 shadow-sm font-bold transition flex items-center gap-1';
|
||
choiceBtn.className = 'px-2.5 py-1.5 rounded-lg text-slate-600 hover:text-slate-900 transition font-semibold flex items-center gap-1';
|
||
}
|
||
|
||
renderQuizQuestion();
|
||
}
|
||
|
||
function startQuiz(mode = 'highlighted') {
|
||
currentQuizMode = mode;
|
||
let pool = [];
|
||
|
||
// Clear any running exam timer
|
||
if (examTimerInterval) {
|
||
clearInterval(examTimerInterval);
|
||
examTimerInterval = null;
|
||
}
|
||
|
||
// Update Quiz Mode buttons UI
|
||
const btnMap = {
|
||
'highlighted': 'quizModeKernBtn',
|
||
'all': 'quizModeAllBtn',
|
||
'zusatz': 'quizModeZusatzBtn',
|
||
'weakspots': 'quizModeWeakBtn',
|
||
'exam': 'quizModeExamBtn'
|
||
};
|
||
|
||
['quizModeKernBtn', 'quizModeAllBtn', 'quizModeZusatzBtn', 'quizModeWeakBtn', 'quizModeExamBtn'].forEach(id => {
|
||
const b = document.getElementById(id);
|
||
if (b) {
|
||
if (id === 'quizModeExamBtn') {
|
||
b.className = 'px-2.5 sm:px-3 py-1.5 rounded-lg bg-slate-200 text-slate-700 hover:bg-slate-300 transition font-bold flex items-center gap-1';
|
||
} else {
|
||
b.className = 'px-2.5 sm:px-3 py-1.5 rounded-lg text-slate-600 hover:text-slate-900 transition font-semibold';
|
||
}
|
||
}
|
||
});
|
||
|
||
const activeBtn = document.getElementById(btnMap[mode]);
|
||
if (activeBtn) {
|
||
if (mode === 'exam') {
|
||
activeBtn.className = 'px-2.5 sm:px-3 py-1.5 rounded-lg bg-gradient-to-r from-amber-500 to-orange-500 text-white shadow font-extrabold transition flex items-center gap-1';
|
||
} else {
|
||
activeBtn.className = 'px-2.5 sm:px-3 py-1.5 rounded-lg bg-white text-indigo-700 shadow-sm font-bold transition';
|
||
}
|
||
}
|
||
|
||
const timerBar = document.getElementById('examTimerBar');
|
||
const examNavBar = document.getElementById('examNavBar');
|
||
const scoreStatsContainer = document.getElementById('quizScoreStatsContainer');
|
||
|
||
if (mode === 'exam') {
|
||
pool = [...STILMITTEL_DATA];
|
||
document.getElementById('quizModeName').innerText = '⏱️ Klausursimulation (15 Fragen unter Zeitdruck)';
|
||
timerBar.classList.remove('hidden');
|
||
examNavBar.classList.remove('hidden');
|
||
scoreStatsContainer.classList.add('hidden');
|
||
|
||
// Start 10-minute timer
|
||
examTimeRemaining = 600;
|
||
examStartTime = Date.now();
|
||
updateExamTimerDisplay();
|
||
examTimerInterval = setInterval(handleExamTimerTick, 1000);
|
||
} else if (mode === 'weakspots') {
|
||
if (quizMistakes.length > 0) {
|
||
pool = STILMITTEL_DATA.filter(item => quizMistakes.includes(item.id));
|
||
} else {
|
||
pool = STILMITTEL_DATA.filter(item => (cardLevels[item.id] || 1) <= 2);
|
||
}
|
||
document.getElementById('quizModeName').innerText = `⚠️ Schwachstellen-Training (${pool.length} Begriffe)`;
|
||
timerBar.classList.add('hidden');
|
||
examNavBar.classList.add('hidden');
|
||
scoreStatsContainer.classList.remove('hidden');
|
||
} else if (mode === 'all') {
|
||
pool = [...STILMITTEL_DATA];
|
||
document.getElementById('quizModeName').innerText = 'Gesamt-Challenge (Alle 23 Stilmittel)';
|
||
timerBar.classList.add('hidden');
|
||
examNavBar.classList.add('hidden');
|
||
scoreStatsContainer.classList.remove('hidden');
|
||
} else if (mode === 'zusatz') {
|
||
pool = STILMITTEL_DATA.filter(item => !item.highlighted);
|
||
document.getElementById('quizModeName').innerText = '🔍 Zusatzinfo-Training (6 erweiterte Stilmittel)';
|
||
timerBar.classList.add('hidden');
|
||
examNavBar.classList.add('hidden');
|
||
scoreStatsContainer.classList.remove('hidden');
|
||
} else {
|
||
// Default: highlighted / Kernstoff (17)
|
||
currentQuizMode = 'highlighted';
|
||
pool = STILMITTEL_DATA.filter(item => item.highlighted);
|
||
document.getElementById('quizModeName').innerText = '🌟 Kernstoff-Challenge (17 Stilmittel)';
|
||
timerBar.classList.add('hidden');
|
||
examNavBar.classList.add('hidden');
|
||
scoreStatsContainer.classList.remove('hidden');
|
||
}
|
||
|
||
if (pool.length === 0) pool = [...STILMITTEL_DATA];
|
||
|
||
pool.sort(() => Math.random() - 0.5);
|
||
const questionCount = mode === 'exam' ? 15 : Math.min(10, pool.length);
|
||
quizQuestions = [];
|
||
|
||
for (let i = 0; i < questionCount; i++) {
|
||
const correct = pool[i % pool.length];
|
||
const randomExample = correct.examples[Math.floor(Math.random() * correct.examples.length)];
|
||
|
||
// Smart distractors: prioritize confusable term if available
|
||
let potentialDistractors = STILMITTEL_DATA.filter(x => x.id !== correct.id);
|
||
let priorityDistractor = null;
|
||
if (correct.confusion && correct.confusion.trap) {
|
||
priorityDistractor = potentialDistractors.find(x => correct.confusion.trap.toLowerCase().includes(x.term.toLowerCase()));
|
||
}
|
||
|
||
let distractors = [];
|
||
if (priorityDistractor) {
|
||
distractors.push(priorityDistractor);
|
||
potentialDistractors = potentialDistractors.filter(x => x.id !== priorityDistractor.id);
|
||
}
|
||
|
||
potentialDistractors.sort(() => Math.random() - 0.5);
|
||
while (distractors.length < 3 && potentialDistractors.length > 0) {
|
||
distractors.push(potentialDistractors.pop());
|
||
}
|
||
|
||
const options = [correct, ...distractors].sort(() => Math.random() - 0.5);
|
||
|
||
quizQuestions.push({
|
||
correctItem: correct,
|
||
example: randomExample,
|
||
options: options,
|
||
userAnswerId: null,
|
||
userAnswerText: '',
|
||
isAnswered: false,
|
||
isCorrect: false
|
||
});
|
||
}
|
||
|
||
currentQuizIndex = 0;
|
||
quizScore = 0;
|
||
quizStreak = 0;
|
||
quizMaxStreak = 0;
|
||
isQuestionAnswered = false;
|
||
|
||
document.getElementById('quizScore').innerText = '0';
|
||
document.getElementById('quizStreak').innerText = '0';
|
||
document.getElementById('quizPlayArea').classList.remove('hidden');
|
||
document.getElementById('quizResultScreen').classList.add('hidden');
|
||
document.getElementById('examResultScreen').classList.add('hidden');
|
||
document.getElementById('quizFeedbackBox').classList.add('hidden');
|
||
|
||
renderQuizQuestion();
|
||
}
|
||
|
||
function startWeakSpotsQuiz() {
|
||
if (quizMistakes.length === 0) {
|
||
const lowCards = STILMITTEL_DATA.filter(item => (cardLevels[item.id] || 1) <= 2);
|
||
if (lowCards.length === 0) {
|
||
showToast('Keine Schwachstellen vorhanden! Starte Kernstoff-Quiz.', 'fa-star');
|
||
startQuiz('highlighted');
|
||
return;
|
||
}
|
||
}
|
||
startQuiz('weakspots');
|
||
}
|
||
|
||
function handleExamTimerTick() {
|
||
examTimeRemaining--;
|
||
updateExamTimerDisplay();
|
||
if (examTimeRemaining <= 0) {
|
||
clearInterval(examTimerInterval);
|
||
examTimerInterval = null;
|
||
showToast('Zeit abgelaufen! Prüfung wird ausgewertet...', 'fa-stopwatch');
|
||
finishExam();
|
||
}
|
||
}
|
||
|
||
function updateExamTimerDisplay() {
|
||
const minutes = Math.floor(Math.max(0, examTimeRemaining) / 60);
|
||
const seconds = Math.max(0, examTimeRemaining) % 60;
|
||
const displayStr = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
|
||
const timerEl = document.getElementById('examTimerDisplay');
|
||
if (timerEl) {
|
||
timerEl.innerText = displayStr;
|
||
if (examTimeRemaining <= 30) {
|
||
timerEl.className = 'font-mono text-2xl font-black text-rose-500 animate-pulse tracking-wider';
|
||
} else if (examTimeRemaining <= 120) {
|
||
timerEl.className = 'font-mono text-2xl font-black text-amber-400 tracking-wider';
|
||
} else {
|
||
timerEl.className = 'font-mono text-2xl font-black text-emerald-400 tracking-wider';
|
||
}
|
||
}
|
||
}
|
||
|
||
function confirmSubmitExamEarly() {
|
||
const answeredCount = quizQuestions.filter(q => q.isAnswered).length;
|
||
const confirmMsg = `Du hast ${answeredCount} von ${quizQuestions.length} Fragen beantwortet.\n\nMöchtest du die Prüfung wirklich jetzt abgeben und auswerten lassen?`;
|
||
if (confirm(confirmMsg)) {
|
||
finishExam();
|
||
}
|
||
}
|
||
|
||
function renderQuizQuestion() {
|
||
if (currentQuizIndex >= quizQuestions.length) {
|
||
if (currentQuizMode === 'exam') {
|
||
finishExam();
|
||
} else {
|
||
finishQuiz();
|
||
}
|
||
return;
|
||
}
|
||
|
||
const q = quizQuestions[currentQuizIndex];
|
||
isQuestionAnswered = q.isAnswered || false;
|
||
|
||
document.getElementById('quizQuestionNumber').innerText = `Frage ${currentQuizIndex + 1} von ${quizQuestions.length}`;
|
||
document.getElementById('quizCategoryTag').innerText = q.correctItem.categoryLabel;
|
||
document.getElementById('quizPhraseBox').innerText = `„${q.example}“`;
|
||
document.getElementById('quizFeedbackBox').classList.add('hidden');
|
||
|
||
const isExam = currentQuizMode === 'exam';
|
||
|
||
// Update Exam Nav Bar
|
||
const examTracker = document.getElementById('examQuestionTracker');
|
||
if (examTracker) examTracker.innerText = `Frage ${currentQuizIndex + 1} von ${quizQuestions.length}`;
|
||
const prevBtn = document.getElementById('examPrevBtn');
|
||
if (prevBtn) prevBtn.disabled = currentQuizIndex === 0;
|
||
const nextBtn = document.getElementById('examNextBtn');
|
||
if (nextBtn) {
|
||
if (currentQuizIndex === quizQuestions.length - 1) {
|
||
nextBtn.innerHTML = '<span>Prüfung abgeben</span> <i class="fa-solid fa-paper-plane text-xs"></i>';
|
||
nextBtn.className = 'px-5 py-2.5 rounded-xl bg-emerald-600 hover:bg-emerald-700 text-white font-bold text-xs sm:text-sm shadow transition flex items-center gap-1.5';
|
||
} else {
|
||
nextBtn.innerHTML = '<span>Weiter</span> <i class="fa-solid fa-chevron-right text-xs"></i>';
|
||
nextBtn.className = 'px-5 py-2.5 rounded-xl bg-slate-900 hover:bg-slate-800 text-white font-bold text-xs sm:text-sm shadow transition flex items-center gap-1.5';
|
||
}
|
||
}
|
||
|
||
const optionsContainer = document.getElementById('quizOptionsContainer');
|
||
const freitextContainer = document.getElementById('quizFreitextContainer');
|
||
|
||
if (currentQuizInputType === 'text') {
|
||
optionsContainer.classList.add('hidden');
|
||
freitextContainer.classList.remove('hidden');
|
||
const input = document.getElementById('quizFreitextInput');
|
||
input.value = q.userAnswerText || '';
|
||
input.disabled = isExam ? false : isQuestionAnswered;
|
||
setTimeout(() => input.focus(), 50);
|
||
} else {
|
||
freitextContainer.classList.add('hidden');
|
||
optionsContainer.classList.remove('hidden');
|
||
optionsContainer.innerHTML = '';
|
||
|
||
q.options.forEach((opt, idx) => {
|
||
const btn = document.createElement('button');
|
||
btn.id = `quiz-opt-btn-${idx}`;
|
||
const isSelected = q.userAnswerId === opt.id;
|
||
|
||
let btnClass = 'quiz-opt-btn w-full p-4 rounded-2xl border-2 font-bold text-sm transition shadow-sm text-left flex items-center justify-between group ';
|
||
if (isExam) {
|
||
btnClass += isSelected
|
||
? 'bg-indigo-50 border-indigo-600 text-indigo-950 ring-2 ring-indigo-300'
|
||
: 'bg-white border-slate-200 hover:border-indigo-400 text-slate-800';
|
||
} else {
|
||
btnClass += 'bg-white border-slate-200 hover:border-indigo-500 hover:bg-indigo-50/50 text-slate-800';
|
||
}
|
||
btn.className = btnClass;
|
||
|
||
btn.innerHTML = `
|
||
<div class="flex items-center gap-3">
|
||
<span class="w-7 h-7 rounded-xl ${isSelected ? 'bg-indigo-600 text-white' : 'bg-slate-100 group-hover:bg-indigo-600 group-hover:text-white'} flex items-center justify-center text-xs font-mono text-slate-600 transition">${idx + 1}</span>
|
||
<span>${opt.term}</span>
|
||
</div>
|
||
<i class="fa-solid ${isSelected ? 'fa-check text-indigo-600' : 'fa-chevron-right text-slate-300 group-hover:text-indigo-600'} text-xs transition"></i>
|
||
`;
|
||
btn.onclick = () => answerQuiz(opt.id, btn);
|
||
optionsContainer.appendChild(btn);
|
||
});
|
||
}
|
||
}
|
||
|
||
function answerQuiz(selectedId, buttonEl) {
|
||
const q = quizQuestions[currentQuizIndex];
|
||
const isExam = currentQuizMode === 'exam';
|
||
|
||
if (isExam) {
|
||
q.userAnswerId = selectedId;
|
||
const chosenOpt = q.options.find(o => o.id === selectedId);
|
||
q.userAnswerText = chosenOpt ? chosenOpt.term : '';
|
||
q.isAnswered = true;
|
||
q.isCorrect = selectedId === q.correctItem.id;
|
||
playTone('tap');
|
||
renderQuizQuestion();
|
||
return;
|
||
}
|
||
|
||
if (isQuestionAnswered) return;
|
||
isQuestionAnswered = true;
|
||
q.isAnswered = true;
|
||
q.userAnswerId = selectedId;
|
||
|
||
const isCorrect = selectedId === q.correctItem.id;
|
||
q.isCorrect = isCorrect;
|
||
|
||
handleAnswerEvaluation(isCorrect, buttonEl);
|
||
}
|
||
|
||
function handleFreitextKeydown(e) {
|
||
if (e.key === 'Enter') {
|
||
e.preventDefault();
|
||
submitFreitextAnswer();
|
||
}
|
||
}
|
||
|
||
function submitFreitextAnswer() {
|
||
const input = document.getElementById('quizFreitextInput');
|
||
const val = input.value.trim();
|
||
if (!val) {
|
||
input.focus();
|
||
return;
|
||
}
|
||
|
||
const q = quizQuestions[currentQuizIndex];
|
||
const match = checkFuzzyMatch(val, q.correctItem.term);
|
||
const isExam = currentQuizMode === 'exam';
|
||
|
||
q.userAnswerText = val;
|
||
q.isAnswered = true;
|
||
q.isCorrect = match.isMatch;
|
||
|
||
if (isExam) {
|
||
playTone('tap');
|
||
showToast('Antwort gespeichert!', 'fa-check');
|
||
if (currentQuizIndex < quizQuestions.length - 1) {
|
||
nextExamQuestion();
|
||
} else {
|
||
renderQuizQuestion();
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (isQuestionAnswered) return;
|
||
isQuestionAnswered = true;
|
||
input.disabled = true;
|
||
|
||
handleAnswerEvaluation(match.isMatch, null, match);
|
||
}
|
||
|
||
function handleAnswerEvaluation(isCorrect, buttonEl, fuzzyResult = null) {
|
||
const q = quizQuestions[currentQuizIndex];
|
||
const feedbackBox = document.getElementById('quizFeedbackBox');
|
||
const feedbackIcon = document.getElementById('quizFeedbackIcon');
|
||
const feedbackTitle = document.getElementById('quizFeedbackTitle');
|
||
const feedbackText = document.getElementById('quizFeedbackText');
|
||
|
||
const buttons = document.querySelectorAll('.quiz-opt-btn');
|
||
buttons.forEach(btn => {
|
||
btn.disabled = true;
|
||
btn.classList.add('pointer-events-none');
|
||
});
|
||
|
||
if (isCorrect) {
|
||
quizScore += 100 + (quizStreak * 20);
|
||
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;
|
||
playTone('success');
|
||
|
||
quizMistakes = quizMistakes.filter(id => id !== q.correctItem.id);
|
||
localStorage.setItem('rhetorik_quiz_mistakes', JSON.stringify(quizMistakes));
|
||
|
||
if (buttonEl) {
|
||
buttonEl.className = 'quiz-opt-btn w-full p-4 rounded-2xl bg-emerald-50 border-2 border-emerald-500 font-bold text-emerald-950 text-sm shadow-md text-left flex items-center justify-between';
|
||
buttonEl.innerHTML += `<i class="fa-solid fa-check text-emerald-600 text-lg"></i>`;
|
||
}
|
||
|
||
feedbackBox.className = 'rounded-2xl p-4 sm:p-5 border bg-emerald-50 border-emerald-200 text-emerald-950 block';
|
||
feedbackIcon.className = 'fa-solid fa-circle-check text-emerald-600 text-2xl mt-0.5';
|
||
|
||
let title = `Exakt richtig! Es ist eine ${q.correctItem.term}.`;
|
||
if (fuzzyResult && !fuzzyResult.exact) {
|
||
title = `Richtig gelöst! (Tippfehler bemerkt: Korrekt schreibt man '${q.correctItem.term}')`;
|
||
}
|
||
feedbackTitle.innerText = title;
|
||
|
||
let correctExtraHtml = `<p>${q.correctItem.definition} (${q.correctItem.tip || ''})</p>`;
|
||
if (q.correctItem.confusion) {
|
||
correctExtraHtml += `
|
||
<div class="mt-2.5 p-2.5 bg-amber-50/90 border border-amber-200 rounded-xl text-xs sm:text-sm text-amber-950">
|
||
<div class="font-bold flex items-center gap-1.5 text-amber-900">
|
||
<i class="fa-solid fa-triangle-exclamation text-amber-600"></i>
|
||
<span>Achtung (${q.correctItem.confusion.trap}):</span>
|
||
</div>
|
||
<p class="mt-0.5 text-slate-700">${q.correctItem.confusion.text}</p>
|
||
</div>
|
||
`;
|
||
}
|
||
feedbackText.innerHTML = correctExtraHtml;
|
||
} else {
|
||
quizStreak = 0;
|
||
playTone('error');
|
||
|
||
if (!quizMistakes.includes(q.correctItem.id)) {
|
||
quizMistakes.push(q.correctItem.id);
|
||
localStorage.setItem('rhetorik_quiz_mistakes', JSON.stringify(quizMistakes));
|
||
}
|
||
|
||
if (buttonEl) {
|
||
buttonEl.className = 'quiz-opt-btn w-full p-4 rounded-2xl bg-rose-50 border-2 border-rose-500 font-bold text-rose-950 text-sm shadow-md text-left flex items-center justify-between';
|
||
buttonEl.innerHTML += `<i class="fa-solid fa-xmark text-rose-600 text-lg"></i>`;
|
||
}
|
||
|
||
buttons.forEach((btn, idx) => {
|
||
if (q.options[idx].id === q.correctItem.id) {
|
||
btn.className = 'quiz-opt-btn w-full p-4 rounded-2xl bg-emerald-50 border-2 border-emerald-400 font-bold text-emerald-900 text-sm shadow-sm text-left flex items-center justify-between';
|
||
btn.innerHTML += `<i class="fa-solid fa-check text-emerald-600 text-base"></i>`;
|
||
}
|
||
});
|
||
|
||
feedbackBox.className = 'rounded-2xl p-4 sm:p-5 border bg-rose-50 border-rose-200 text-rose-950 block';
|
||
feedbackIcon.className = 'fa-solid fa-circle-xmark text-rose-600 text-2xl mt-0.5';
|
||
feedbackTitle.innerText = `Leider falsch. Richtig wäre: ${q.correctItem.term}.`;
|
||
|
||
let wrongExtraHtml = `<p>${q.correctItem.definition} <strong>Merke:</strong> ${q.correctItem.tip || ''}</p>`;
|
||
if (q.correctItem.confusion) {
|
||
wrongExtraHtml += `
|
||
<div class="mt-2.5 p-2.5 bg-amber-50/90 border border-amber-200 rounded-xl text-xs sm:text-sm text-amber-950">
|
||
<div class="font-bold flex items-center gap-1.5 text-amber-900">
|
||
<i class="fa-solid fa-triangle-exclamation text-amber-600"></i>
|
||
<span>Typische Falle (${q.correctItem.confusion.trap}):</span>
|
||
</div>
|
||
<p class="mt-0.5 text-slate-700">${q.correctItem.confusion.text}</p>
|
||
</div>
|
||
`;
|
||
}
|
||
if (q.correctItem.analysisTemplate) {
|
||
wrongExtraHtml += `
|
||
<div class="mt-2 p-2 bg-indigo-50 border border-indigo-100 rounded-xl text-xs text-indigo-950 font-serif italic">
|
||
<strong>Klausur-Muster:</strong> ${q.correctItem.analysisTemplate}
|
||
</div>
|
||
`;
|
||
}
|
||
feedbackText.innerHTML = wrongExtraHtml;
|
||
}
|
||
|
||
document.getElementById('quizScore').innerText = quizScore;
|
||
document.getElementById('quizStreak').innerText = quizStreak;
|
||
updateQuizMistakesBadge();
|
||
}
|
||
|
||
function nextQuizQuestion() {
|
||
currentQuizIndex++;
|
||
renderQuizQuestion();
|
||
}
|
||
|
||
function prevExamQuestion() {
|
||
if (currentQuizIndex > 0) {
|
||
currentQuizIndex--;
|
||
renderQuizQuestion();
|
||
}
|
||
}
|
||
|
||
function nextExamQuestion() {
|
||
if (currentQuizIndex < quizQuestions.length - 1) {
|
||
currentQuizIndex++;
|
||
renderQuizQuestion();
|
||
} else {
|
||
confirmSubmitExamEarly();
|
||
}
|
||
}
|
||
|
||
function finishExam() {
|
||
if (examTimerInterval) {
|
||
clearInterval(examTimerInterval);
|
||
examTimerInterval = null;
|
||
}
|
||
|
||
const elapsedSec = Math.round((Date.now() - (examStartTime || Date.now())) / 1000);
|
||
const minutes = Math.floor(elapsedSec / 60);
|
||
const seconds = elapsedSec % 60;
|
||
const timeStr = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')} Min`;
|
||
|
||
let correctCount = 0;
|
||
quizQuestions.forEach(q => {
|
||
let isRight = false;
|
||
if (currentQuizInputType === 'text') {
|
||
isRight = checkFuzzyMatch(q.userAnswerText || '', q.correctItem.term).isMatch;
|
||
} else {
|
||
isRight = q.userAnswerId === q.correctItem.id;
|
||
}
|
||
q.isCorrect = isRight;
|
||
if (isRight) correctCount++;
|
||
else {
|
||
if (!quizMistakes.includes(q.correctItem.id)) {
|
||
quizMistakes.push(q.correctItem.id);
|
||
}
|
||
}
|
||
});
|
||
|
||
localStorage.setItem('rhetorik_quiz_mistakes', JSON.stringify(quizMistakes));
|
||
updateQuizMistakesBadge();
|
||
|
||
const gradeInfo = calculateExamGrade(correctCount, quizQuestions.length);
|
||
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
|
||
document.getElementById('quizPlayArea').classList.add('hidden');
|
||
document.getElementById('examTimerBar').classList.add('hidden');
|
||
document.getElementById('examResultScreen').classList.remove('hidden');
|
||
|
||
const badgeContainer = document.getElementById('examGradeBadgeContainer');
|
||
badgeContainer.className = `inline-flex items-center gap-2 px-6 py-2.5 rounded-2xl border-2 font-black text-xl sm:text-2xl shadow-sm ${gradeInfo.badgeClass}`;
|
||
document.getElementById('examGradeTitle').innerText = `Note: ${gradeInfo.name}`;
|
||
const iconEl = document.getElementById('examGradeIcon');
|
||
iconEl.className = `fa-solid ${gradeInfo.icon} ${gradeInfo.iconColor}`;
|
||
|
||
document.getElementById('examPedagogicFeedback').innerText = gradeInfo.feedback;
|
||
document.getElementById('examScoreDisplay').innerText = `${correctCount} / ${quizQuestions.length}`;
|
||
document.getElementById('examPercentDisplay').innerText = `${Math.round((correctCount / quizQuestions.length) * 100)}%`;
|
||
document.getElementById('examTimeDisplay').innerText = timeStr;
|
||
|
||
const repeatMistakesBtn = document.getElementById('examRepeatMistakesBtn');
|
||
if (quizQuestions.some(q => !q.isCorrect)) {
|
||
repeatMistakesBtn.classList.remove('hidden');
|
||
} else {
|
||
repeatMistakesBtn.classList.add('hidden');
|
||
}
|
||
|
||
// Render Itemized Review List
|
||
const reviewList = document.getElementById('examReviewList');
|
||
reviewList.innerHTML = '';
|
||
|
||
quizQuestions.forEach((q, idx) => {
|
||
const itemEl = document.createElement('div');
|
||
itemEl.className = `p-4 rounded-2xl border ${q.isCorrect ? 'bg-emerald-50/60 border-emerald-200' : 'bg-rose-50/60 border-rose-200'} space-y-2`;
|
||
|
||
const userAns = q.userAnswerText || (q.userAnswerId ? (q.options.find(o => o.id === q.userAnswerId)?.term || 'Unbekannt') : '<Keine Antwort gegeben>');
|
||
|
||
itemEl.innerHTML = `
|
||
<div class="flex items-start justify-between gap-2">
|
||
<div>
|
||
<span class="font-extrabold text-xs uppercase tracking-wider ${q.isCorrect ? 'text-emerald-700' : 'text-rose-700'}">Frage ${idx + 1} (${q.correctItem.categoryLabel})</span>
|
||
<p class="font-serif italic text-sm sm:text-base text-slate-900 mt-0.5">„${q.example}“</p>
|
||
</div>
|
||
<span class="px-2.5 py-1 rounded-xl text-xs font-bold ${q.isCorrect ? 'bg-emerald-200 text-emerald-950' : 'bg-rose-200 text-rose-950'} flex items-center gap-1">
|
||
<i class="fa-solid ${q.isCorrect ? 'fa-check' : 'fa-xmark'}"></i>
|
||
<span>${q.isCorrect ? 'Richtig' : 'Falsch'}</span>
|
||
</span>
|
||
</div>
|
||
|
||
<div class="pt-1 text-xs border-t border-slate-200/60 flex flex-wrap items-center gap-x-4 gap-y-1">
|
||
<div><span class="text-slate-500 font-bold">Deine Antwort:</span> <span class="${q.isCorrect ? 'text-emerald-800' : 'text-rose-700'} font-bold">${userAns}</span></div>
|
||
<div><span class="text-slate-500 font-bold">Lösung:</span> <span class="text-slate-900 font-extrabold">${q.correctItem.term}</span></div>
|
||
</div>
|
||
|
||
<p class="text-xs text-slate-600 leading-snug">${q.correctItem.definition}</p>
|
||
|
||
${!q.isCorrect && q.correctItem.confusion ? `
|
||
<div class="p-2 bg-amber-50 rounded-xl border border-amber-200 text-[11px] text-amber-950">
|
||
<strong>⚠️ Verwechslungsgefahr (${q.correctItem.confusion.trap}):</strong> ${q.correctItem.confusion.text}
|
||
</div>
|
||
` : ''}
|
||
|
||
${!q.isCorrect && q.correctItem.analysisTemplate ? `
|
||
<div class="p-2 bg-indigo-50/80 rounded-xl border border-indigo-100 text-[11px] text-indigo-950 font-serif italic">
|
||
<strong>📝 Klausur-Muster:</strong> ${q.correctItem.analysisTemplate}
|
||
</div>
|
||
` : ''}
|
||
`;
|
||
reviewList.appendChild(itemEl);
|
||
});
|
||
}
|
||
|
||
function finishQuiz() {
|
||
playTone('win');
|
||
document.getElementById('quizPlayArea').classList.add('hidden');
|
||
const resultScreen = document.getElementById('quizResultScreen');
|
||
resultScreen.classList.remove('hidden');
|
||
|
||
const correctCount = Math.round(quizScore / 100);
|
||
document.getElementById('resultScore').innerText = `${quizScore} Pkt`;
|
||
document.getElementById('resultPercent').innerText = `${Math.min(100, Math.round((correctCount / quizQuestions.length) * 100))}%`;
|
||
document.getElementById('resultMaxStreak').innerText = quizMaxStreak;
|
||
|
||
const repeatBtn = document.getElementById('repeatMistakesBtn');
|
||
if (quizMistakes.length > 0) {
|
||
repeatBtn.classList.remove('hidden');
|
||
} else {
|
||
repeatBtn.classList.add('hidden');
|
||
}
|
||
}
|
||
// ==========================================
|
||
// PHRASEN-MATCHER GAME ENGINE ("Falsch ist falsch")
|
||
// ==========================================
|
||
let matcherFilter = 'highlighted'; // 'highlighted' (bevorzugt) | 'all' | 'zusatz'
|
||
let matcherSelectedPhrase = null;
|
||
let matcherSelectedTerm = null;
|
||
let matcherCorrectCount = 0;
|
||
let matcherFailedCount = 0;
|
||
|
||
function setMatcherFilter(filter) {
|
||
matcherFilter = filter;
|
||
['matcherFilterKernBtn', 'matcherFilterAllBtn', 'matcherFilterZusatzBtn'].forEach(id => {
|
||
const el = document.getElementById(id);
|
||
if (el) el.className = 'px-2.5 sm:px-3 py-1.5 rounded-lg text-slate-600 hover:text-slate-900 transition font-semibold';
|
||
});
|
||
|
||
const activeMap = {
|
||
'highlighted': 'matcherFilterKernBtn',
|
||
'all': 'matcherFilterAllBtn',
|
||
'zusatz': 'matcherFilterZusatzBtn'
|
||
};
|
||
const activeEl = document.getElementById(activeMap[filter]);
|
||
if (activeEl) {
|
||
activeEl.className = 'px-2.5 sm:px-3 py-1.5 rounded-lg bg-white text-indigo-700 shadow-sm font-bold transition';
|
||
}
|
||
|
||
initMatcherGame();
|
||
}
|
||
|
||
function initMatcherGame() {
|
||
matcherSelectedPhrase = null;
|
||
matcherSelectedTerm = null;
|
||
matcherCorrectCount = 0;
|
||
matcherFailedCount = 0;
|
||
document.getElementById('matchScore').innerText = '0';
|
||
document.getElementById('matchFailScore').innerText = '0';
|
||
document.getElementById('matcherResultBanner').classList.add('hidden');
|
||
|
||
let pool = STILMITTEL_DATA;
|
||
if (matcherFilter === 'highlighted') {
|
||
pool = STILMITTEL_DATA.filter(x => x.highlighted);
|
||
} else if (matcherFilter === 'zusatz') {
|
||
pool = STILMITTEL_DATA.filter(x => !x.highlighted);
|
||
}
|
||
if (pool.length < 5) pool = [...STILMITTEL_DATA];
|
||
|
||
const shuffled = [...pool].sort(() => Math.random() - 0.5).slice(0, 5);
|
||
currentMatcherPairs = shuffled;
|
||
|
||
// Pick randomly from all 10 examples for rich variation
|
||
const phrases = shuffled.map(x => ({
|
||
id: x.id,
|
||
term: x.term,
|
||
text: x.examples[Math.floor(Math.random() * x.examples.length)],
|
||
resolved: false
|
||
})).sort(() => Math.random() - 0.5);
|
||
|
||
const terms = shuffled.map(x => ({
|
||
id: x.id,
|
||
name: x.term,
|
||
resolved: false
|
||
})).sort(() => Math.random() - 0.5);
|
||
|
||
const phrasesContainer = document.getElementById('matcherPhrasesContainer');
|
||
const termsContainer = document.getElementById('matcherTermsContainer');
|
||
|
||
phrasesContainer.innerHTML = '';
|
||
termsContainer.innerHTML = '';
|
||
|
||
phrases.forEach(p => {
|
||
const card = document.createElement('div');
|
||
card.id = `match-phrase-${p.id}`;
|
||
card.className = 'match-phrase-card p-4 bg-white rounded-2xl border-2 border-slate-200 text-sm font-serif italic text-slate-800 cursor-pointer hover:border-indigo-400 hover:bg-slate-50 transition shadow-sm select-none flex items-start justify-between gap-2';
|
||
|
||
const cleanSpeechText = p.text.replace(/'/g, "\\'").replace(/"/g, '"');
|
||
card.innerHTML = `
|
||
<div class="leading-relaxed">„${p.text}“</div>
|
||
<button onclick="speakText('${cleanSpeechText}', event)" title="Vorlesen" class="p-1 rounded-lg text-slate-400 hover:text-indigo-600 hover:bg-indigo-50 transition shrink-0 not-italic">
|
||
<i class="fa-solid fa-volume-high text-xs"></i>
|
||
</button>
|
||
`;
|
||
card.onclick = () => selectMatcherPhrase(p.id, card, p.term, p.text);
|
||
phrasesContainer.appendChild(card);
|
||
});
|
||
|
||
terms.forEach(t => {
|
||
const card = document.createElement('div');
|
||
card.id = `match-term-${t.id}`;
|
||
card.className = 'match-term-card p-4 bg-white rounded-2xl border-2 border-slate-200 text-sm font-sans font-bold text-slate-800 cursor-pointer hover:border-indigo-400 hover:bg-slate-50 transition shadow-sm select-none flex items-center justify-between';
|
||
card.innerHTML = `<span>${t.name}</span><i class="fa-solid fa-link text-slate-300 text-xs"></i>`;
|
||
card.onclick = () => selectMatcherTerm(t.id, card, t.name);
|
||
termsContainer.appendChild(card);
|
||
});
|
||
}
|
||
|
||
function selectMatcherPhrase(id, el, term, text) {
|
||
if (el.classList.contains('pointer-events-none')) return;
|
||
document.querySelectorAll('.match-phrase-card').forEach(c => {
|
||
if (!c.classList.contains('pointer-events-none')) {
|
||
c.classList.remove('border-indigo-600', 'bg-indigo-50', 'ring-2', 'ring-indigo-300');
|
||
c.classList.add('border-slate-200');
|
||
}
|
||
});
|
||
matcherSelectedPhrase = { id, el, term, text };
|
||
el.classList.remove('border-slate-200');
|
||
el.classList.add('border-indigo-600', 'bg-indigo-50', 'ring-2', 'ring-indigo-300');
|
||
playTone('flip');
|
||
checkMatcherMatch();
|
||
}
|
||
|
||
function selectMatcherTerm(id, el, name) {
|
||
if (el.classList.contains('pointer-events-none')) return;
|
||
document.querySelectorAll('.match-term-card').forEach(c => {
|
||
if (!c.classList.contains('pointer-events-none')) {
|
||
c.classList.remove('border-indigo-600', 'bg-indigo-50', 'ring-2', 'ring-indigo-300');
|
||
c.classList.add('border-slate-200');
|
||
}
|
||
});
|
||
matcherSelectedTerm = { id, el, name };
|
||
el.classList.remove('border-slate-200');
|
||
el.classList.add('border-indigo-600', 'bg-indigo-50', 'ring-2', 'ring-indigo-300');
|
||
playTone('flip');
|
||
checkMatcherMatch();
|
||
}
|
||
|
||
function checkMatcherMatch() {
|
||
if (!matcherSelectedPhrase || !matcherSelectedTerm) return;
|
||
|
||
const pObj = matcherSelectedPhrase;
|
||
const tObj = matcherSelectedTerm;
|
||
const isMatch = pObj.id === tObj.id;
|
||
|
||
if (isMatch) {
|
||
// Richtig zugeordnet!
|
||
playTone('success');
|
||
matcherCorrectCount++;
|
||
document.getElementById('matchScore').innerText = matcherCorrectCount;
|
||
|
||
// Phrase dauerhaft als RICHTIG sperren
|
||
pObj.el.className = 'p-4 bg-emerald-50 border-2 border-emerald-500 text-sm font-serif italic text-emerald-950 rounded-2xl transition shadow-sm pointer-events-none flex items-start justify-between gap-2';
|
||
pObj.el.innerHTML = `
|
||
<div class="leading-relaxed">„${pObj.text}“</div>
|
||
<span class="px-2 py-0.5 rounded-lg bg-emerald-200 text-emerald-950 text-xs font-sans font-bold flex items-center gap-1 shrink-0">
|
||
<i class="fa-solid fa-check"></i> Richtig
|
||
</span>
|
||
`;
|
||
|
||
// Begriff dauerhaft als RICHTIG sperren
|
||
tObj.el.className = 'p-4 bg-emerald-50 border-2 border-emerald-500 text-sm font-sans font-bold text-emerald-950 rounded-2xl transition shadow-sm pointer-events-none flex items-center justify-between';
|
||
tObj.el.innerHTML = `<span>${tObj.name}</span><i class="fa-solid fa-check text-emerald-600 text-base"></i>`;
|
||
|
||
addXP(20, 'Matcher Paar gelöst');
|
||
matcherSelectedPhrase = null;
|
||
matcherSelectedTerm = null;
|
||
} else {
|
||
// "FALSCH IST FALSCH!" - Ein Fehlversuch verbrennt dieses Zitat unwiderruflich!
|
||
playTone('error');
|
||
matcherFailedCount++;
|
||
document.getElementById('matchFailScore').innerText = matcherFailedCount;
|
||
|
||
// Phrase DAUERHAFT als FALSCH sperren und Lösung aufdecken
|
||
pObj.el.className = 'p-4 bg-rose-50 border-2 border-rose-500 text-sm font-serif italic text-rose-950 rounded-2xl transition shadow-sm pointer-events-none space-y-1.5';
|
||
pObj.el.innerHTML = `
|
||
<div class="leading-relaxed">„${pObj.text}“</div>
|
||
<div class="pt-1.5 border-t border-rose-200/80 flex items-center justify-between font-sans text-xs">
|
||
<span class="text-rose-700 font-extrabold flex items-center gap-1"><i class="fa-solid fa-xmark"></i> Falsch!</span>
|
||
<span class="text-slate-700 font-medium">Lösung: <strong class="text-indigo-950 font-black">${pObj.term}</strong></span>
|
||
</div>
|
||
`;
|
||
|
||
// Begriff blinkt kurz rot auf und wird dann wieder neutral freigegeben (für andere Zitate)
|
||
tObj.el.classList.add('border-rose-500', 'bg-rose-50');
|
||
setTimeout(() => {
|
||
tObj.el.classList.remove('border-rose-500', 'bg-rose-50', 'border-indigo-600', 'bg-indigo-50', 'ring-2', 'ring-indigo-300');
|
||
tObj.el.classList.add('border-slate-200');
|
||
}, 450);
|
||
|
||
// Als Schwachstelle speichern für gezieltes Nachlernen
|
||
if (!quizMistakes.includes(pObj.id)) {
|
||
quizMistakes.push(pObj.id);
|
||
localStorage.setItem('rhetorik_quiz_mistakes', JSON.stringify(quizMistakes));
|
||
updateQuizMistakesBadge();
|
||
}
|
||
|
||
matcherSelectedPhrase = null;
|
||
matcherSelectedTerm = null;
|
||
}
|
||
|
||
// Prüfen, ob alle 5 Paare versucht wurden (richtig + falsch = 5)
|
||
if (matcherCorrectCount + matcherFailedCount === 5) {
|
||
setTimeout(finishMatcherRound, 500);
|
||
}
|
||
}
|
||
|
||
function finishMatcherRound() {
|
||
const banner = document.getElementById('matcherResultBanner');
|
||
const title = document.getElementById('matcherResultTitle');
|
||
const subtitle = document.getElementById('matcherResultSubtitle');
|
||
const icon = document.getElementById('matcherResultIcon');
|
||
const weakBtn = document.getElementById('matcherWeakspotsBtn');
|
||
|
||
banner.classList.remove('hidden', 'bg-emerald-600', 'bg-indigo-600', 'bg-slate-900', 'bg-rose-600');
|
||
|
||
if (matcherCorrectCount === 5) {
|
||
playTone('win');
|
||
banner.classList.add('bg-emerald-600');
|
||
icon.innerHTML = '<i class="fa-solid fa-crown text-amber-300 text-5xl"></i>';
|
||
title.innerText = 'Perfekt zugeordnet! 🏆';
|
||
subtitle.innerText = 'Alle 5 Paare auf Anhieb fehlerfrei erkannt. Keine Fehler gemacht!';
|
||
addXP(60, 'Matcher fehlerfrei');
|
||
weakBtn.classList.add('hidden');
|
||
} else if (matcherCorrectCount >= 3) {
|
||
playTone(matcherCorrectCount >= 4 ? 'win' : 'flip');
|
||
banner.classList.add('bg-indigo-600');
|
||
icon.innerHTML = '<i class="fa-solid fa-trophy text-amber-300 text-5xl"></i>';
|
||
title.innerText = `Runde beendet: ${matcherCorrectCount} von 5 richtig!`;
|
||
subtitle.innerText = `Falsch ist falsch: ${matcherFailedCount} Fehler gemacht. Die verpassten Begriffe wurden in deine Schwachstellen übernommen.`;
|
||
weakBtn.classList.remove('hidden');
|
||
} else {
|
||
playTone('error');
|
||
banner.classList.add('bg-slate-900');
|
||
icon.innerHTML = '<i class="fa-solid fa-triangle-exclamation text-rose-400 text-5xl"></i>';
|
||
title.innerText = `Ergebnis: ${matcherCorrectCount} von 5 Paaren richtig`;
|
||
subtitle.innerText = `Falsch ist falsch: ${matcherFailedCount} Fehlversuche. Wiederhole die Begriffe gezielt im Schwachstellen-Trainer oder Glossar.`;
|
||
weakBtn.classList.remove('hidden');
|
||
}
|
||
}
|
||
// GLOSSAR & SUCHE
|
||
function setGlossaryFilter(filter) {
|
||
currentGlossaryFilter = filter;
|
||
const btns = ['filterAllBtn', 'filterKernBtn', 'filterZusatzBtn', 'filterKlangBtn', 'filterSyntaxBtn', 'filterWortBtn', 'filterGedankeBtn'];
|
||
btns.forEach(id => {
|
||
const el = document.getElementById(id);
|
||
if (el) el.className = 'px-3 py-1.5 rounded-lg text-xs font-semibold bg-slate-100 text-slate-600 hover:bg-slate-200 border border-slate-200';
|
||
});
|
||
|
||
const activeMap = {
|
||
'all': 'filterAllBtn',
|
||
'highlight': 'filterKernBtn',
|
||
'zusatz': 'filterZusatzBtn',
|
||
'klang': 'filterKlangBtn',
|
||
'syntax': 'filterSyntaxBtn',
|
||
'wort': 'filterWortBtn',
|
||
'gedanke': 'filterGedankeBtn'
|
||
};
|
||
const activeEl = document.getElementById(activeMap[filter]);
|
||
if (activeEl) {
|
||
activeEl.className = 'px-3 py-1.5 rounded-lg text-xs font-bold bg-indigo-50 text-indigo-700 border border-indigo-200';
|
||
}
|
||
|
||
renderGlossary();
|
||
}
|
||
|
||
function renderGlossary() {
|
||
const searchVal = document.getElementById('glossarySearchInput').value.toLowerCase().trim();
|
||
const grid = document.getElementById('glossaryGrid');
|
||
|
||
let filtered = STILMITTEL_DATA.filter(item => {
|
||
if (currentGlossaryFilter === 'highlight' && !item.highlighted) return false;
|
||
if (currentGlossaryFilter === 'zusatz' && item.highlighted) return false;
|
||
if (['klang', 'syntax', 'wort', 'gedanke'].includes(currentGlossaryFilter) && item.category !== currentGlossaryFilter) return false;
|
||
|
||
if (!searchVal) return true;
|
||
const inTerm = item.term.toLowerCase().includes(searchVal);
|
||
const inDef = item.definition.toLowerCase().includes(searchVal);
|
||
const inEx = item.examples.some(e => e.toLowerCase().includes(searchVal));
|
||
const inTip = (item.tip || '').toLowerCase().includes(searchVal);
|
||
return inTerm || inDef || inEx || inTip;
|
||
});
|
||
|
||
document.getElementById('glossaryResultCount').innerText = `${filtered.length} von ${STILMITTEL_DATA.length} Stilmitteln gefunden`;
|
||
grid.innerHTML = '';
|
||
|
||
if (filtered.length === 0) {
|
||
grid.innerHTML = `
|
||
<div class="col-span-full py-12 text-center text-slate-400">
|
||
<i class="fa-solid fa-search text-3xl mb-2"></i>
|
||
<p>Kein passendes Stilmittel gefunden für „${searchVal}“.</p>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
filtered.forEach(item => {
|
||
const card = document.createElement('div');
|
||
const level = getItemLevel(item.id);
|
||
card.className = 'bg-white rounded-2xl p-5 border border-slate-200 shadow-sm hover:shadow-md hover:border-indigo-300 transition duration-150 flex flex-col justify-between cursor-pointer';
|
||
card.onclick = () => openDetailModal(item.id);
|
||
|
||
card.innerHTML = `
|
||
<div>
|
||
<div class="flex items-center justify-between mb-2">
|
||
<span class="text-base font-extrabold text-slate-900 group-hover:text-indigo-600">${item.term}</span>
|
||
<div class="flex items-center gap-1.5">
|
||
<span class="text-[10px] font-semibold px-2 py-0.5 rounded-full bg-slate-100 text-slate-600">${item.categoryLabel}</span>
|
||
${item.highlighted
|
||
? '<span class="text-[10px] uppercase font-bold px-2 py-0.5 rounded-full bg-indigo-50 text-indigo-700 border border-indigo-200">🌟 Kernstoff</span>'
|
||
: '<span class="text-[10px] uppercase font-bold px-2 py-0.5 rounded-full bg-slate-100 text-slate-500 border border-slate-200">🔍 Zusatz</span>'
|
||
}
|
||
</div>
|
||
</div>
|
||
<p class="text-xs text-slate-600 line-clamp-2 leading-relaxed mb-3">${item.definition}</p>
|
||
</div>
|
||
|
||
<div class="pt-3 border-t border-slate-100 flex items-center justify-between text-xs">
|
||
<p class="font-serif italic text-indigo-900 truncate max-w-[70%]">„${item.examples[0]}“</p>
|
||
<span class="text-[10px] font-bold px-2 py-0.5 rounded-md ${level >= 5 ? 'bg-emerald-100 text-emerald-800' : 'bg-slate-100 text-slate-500'}">
|
||
Kasten ${level}
|
||
</span>
|
||
</div>
|
||
`;
|
||
grid.appendChild(card);
|
||
});
|
||
}
|
||
|
||
function openDetailModal(id) {
|
||
const item = STILMITTEL_DATA.find(x => x.id === id);
|
||
if (!item) return;
|
||
|
||
document.getElementById('modalTerm').innerText = item.term;
|
||
document.getElementById('modalDefinition').innerText = item.definition;
|
||
document.getElementById('modalCategoryBadge').innerText = item.categoryLabel;
|
||
document.getElementById('modalTip').innerText = item.tip || 'Achte bei der Textanalyse auf den Wirkungszusammenhang mit dem Hauptthema.';
|
||
|
||
const badge = document.getElementById('modalHighlightBadge');
|
||
if (item.highlighted) {
|
||
badge.innerText = '🌟 Kernstoff (Prüfungsrelevant)';
|
||
badge.className = 'text-xs font-bold px-2.5 py-0.5 rounded-full bg-indigo-50 text-indigo-700 border border-indigo-200 uppercase tracking-wider';
|
||
} else {
|
||
badge.innerText = '🔍 Zusatzinformation (Vertiefung)';
|
||
badge.className = 'text-xs font-bold px-2.5 py-0.5 rounded-full bg-slate-100 text-slate-600 border border-slate-200 uppercase tracking-wider';
|
||
}
|
||
|
||
const exHtml = `
|
||
<div class="space-y-1.5 max-h-64 overflow-y-auto pr-1">
|
||
${item.examples.map(ex => {
|
||
const cleanEx = ex.replace(/'/g, "\\'").replace(/"/g, '"');
|
||
return `
|
||
<div class="py-1.5 px-2 rounded-xl hover:bg-white flex items-start justify-between gap-2 transition group border border-transparent hover:border-indigo-100">
|
||
<div class="flex items-start gap-2">
|
||
<span class="text-indigo-500 font-bold text-sm select-none mt-0.5">•</span>
|
||
<span class="text-slate-800 leading-snug font-serif text-sm sm:text-base">„${ex}“</span>
|
||
</div>
|
||
<button onclick="speakText('${cleanEx}', event)" title="Dieses Beispiel vorlesen" class="p-1 rounded-lg text-slate-400 group-hover:text-indigo-600 hover:bg-indigo-50 transition shrink-0">
|
||
<i class="fa-solid fa-volume-high text-xs"></i>
|
||
</button>
|
||
</div>
|
||
`;
|
||
}).join('')}
|
||
</div>
|
||
`;
|
||
document.getElementById('modalExamples').innerHTML = exHtml;
|
||
|
||
// Verwechslungsgefahr
|
||
const confusionContainer = document.getElementById('modalConfusionContainer');
|
||
if (item.confusion && item.confusion.trap) {
|
||
confusionContainer.classList.remove('hidden');
|
||
document.getElementById('modalConfusionTrap').innerText = item.confusion.trap;
|
||
document.getElementById('modalConfusionText').innerText = item.confusion.text;
|
||
} else {
|
||
confusionContainer.classList.add('hidden');
|
||
}
|
||
|
||
// Klausur-Formulierungshilfe
|
||
const analysisContainer = document.getElementById('modalAnalysisContainer');
|
||
if (item.analysisTemplate) {
|
||
analysisContainer.classList.remove('hidden');
|
||
document.getElementById('modalAnalysisTemplate').innerText = item.analysisTemplate;
|
||
const copyBtnText = document.getElementById('copyBtnText');
|
||
if (copyBtnText) copyBtnText.innerText = 'Kopieren';
|
||
} else {
|
||
analysisContainer.classList.add('hidden');
|
||
}
|
||
|
||
playTone('flip');
|
||
document.getElementById('detailModal').classList.remove('hidden');
|
||
}
|
||
|
||
function copyAnalysisTemplate() {
|
||
const text = document.getElementById('modalAnalysisTemplate').innerText;
|
||
if (!text) return;
|
||
navigator.clipboard.writeText(text).then(() => {
|
||
const copyBtnText = document.getElementById('copyBtnText');
|
||
if (copyBtnText) copyBtnText.innerText = 'Kopiert!';
|
||
showToast('Muster in die Zwischenablage kopiert!', 'fa-copy');
|
||
setTimeout(() => {
|
||
if (copyBtnText) copyBtnText.innerText = 'Kopieren';
|
||
}, 2000);
|
||
}).catch(() => {
|
||
showToast('Kopieren fehlgeschlagen', 'fa-triangle-exclamation');
|
||
});
|
||
}
|
||
|
||
function closeDetailModal() {
|
||
document.getElementById('detailModal').classList.add('hidden');
|
||
}
|
||
|
||
// PRINT & CHEATSHEET
|
||
function renderPrintCheatsheet() {
|
||
const tbody = document.getElementById('printTableBody');
|
||
if (!tbody) return;
|
||
tbody.innerHTML = '';
|
||
|
||
STILMITTEL_DATA.forEach(item => {
|
||
const row = document.createElement('tr');
|
||
row.innerHTML = `
|
||
<td class="font-bold text-slate-900 align-top">
|
||
<div class="flex items-center gap-1.5">
|
||
<span>${item.term}</span>
|
||
${item.highlighted ? '<span class="text-[8px] bg-indigo-100 text-indigo-800 px-1 py-0.2 rounded font-bold">KERN</span>' : '<span class="text-[8px] bg-slate-100 text-slate-500 px-1 py-0.2 rounded">ZUSATZ</span>'}
|
||
</div>
|
||
<div class="text-[9px] font-normal text-slate-500 uppercase">${item.categoryLabel}</div>
|
||
</td>
|
||
<td class="align-top leading-tight text-slate-800">
|
||
<div>${item.definition}</div>
|
||
${item.analysisTemplate ? `<div class="mt-1 text-[8.5px] text-indigo-900 bg-indigo-50/70 p-1.5 rounded border border-indigo-100 font-serif italic"><strong>Klausur-Muster:</strong> ${item.analysisTemplate}</div>` : ''}
|
||
</td>
|
||
<td class="font-serif italic align-top text-slate-800 leading-tight text-[9.5px]">
|
||
${item.examples.slice(0, 4).map(e => `• „${e}“`).join('<br>')}
|
||
<div class="mt-1 text-[8px] text-slate-400 font-sans not-italic font-medium">+ 6 weitere Beispiele in der App</div>
|
||
</td>
|
||
<td class="text-slate-600 align-top text-[9px] space-y-1">
|
||
<div><strong>Signal:</strong> ${item.tip || '-'}</div>
|
||
${item.confusion ? `<div class="text-amber-900 bg-amber-50 p-1 rounded border border-amber-200 leading-tight"><strong>⚠️ Falle (${item.confusion.trap}):</strong> ${item.confusion.text}</div>` : ''}
|
||
</td>
|
||
`;
|
||
tbody.appendChild(row);
|
||
});
|
||
}
|
||
|
||
function printCheatsheet() {
|
||
renderPrintCheatsheet();
|
||
window.print();
|
||
}
|
||
|
||
function showToast(text, iconClass = 'fa-circle-check') {
|
||
const toast = document.getElementById('toastNotification');
|
||
document.getElementById('toastMessage').innerText = text;
|
||
document.getElementById('toastIcon').className = `fa-solid ${iconClass} text-emerald-400 text-base`;
|
||
toast.classList.remove('translate-y-20', 'opacity-0');
|
||
|
||
setTimeout(() => {
|
||
toast.classList.add('translate-y-20', 'opacity-0');
|
||
}, 2500);
|
||
}
|
||
|
||
function switchTab(tabId) {
|
||
currentTabIndex = tabId;
|
||
|
||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||
btn.classList.remove('bg-indigo-50', 'text-indigo-700', 'font-semibold', 'shadow-sm');
|
||
btn.classList.add('text-slate-600');
|
||
});
|
||
const activeBtn = document.getElementById(`tab-btn-${tabId}`);
|
||
if (activeBtn) {
|
||
activeBtn.classList.add('bg-indigo-50', 'text-indigo-700', 'font-semibold', 'shadow-sm');
|
||
activeBtn.classList.remove('text-slate-600');
|
||
}
|
||
|
||
['cards', 'quiz', 'matcher', 'glossary'].forEach(id => {
|
||
const view = document.getElementById(`view-${id}`);
|
||
if (view) view.classList.add('hidden');
|
||
});
|
||
|
||
const targetView = document.getElementById(`view-${tabId}`);
|
||
if (targetView) targetView.classList.remove('hidden');
|
||
|
||
if (tabId === 'quiz' && quizQuestions.length === 0) {
|
||
startQuiz('highlighted');
|
||
} else if (tabId === 'matcher' && currentMatcherPairs.length === 0) {
|
||
initMatcherGame();
|
||
} else if (tabId === 'glossary') {
|
||
renderGlossary();
|
||
}
|
||
}
|
||
|
||
|
||
// ==========================================
|
||
// SYNC & BACKUP MANAGEMENT (QR & JSON)
|
||
// ==========================================
|
||
let pendingIncomingSync = null;
|
||
|
||
function getSyncPayload() {
|
||
return {
|
||
v: 2,
|
||
timestamp: Date.now(),
|
||
algo: cardAlgo,
|
||
levels: cardLevels,
|
||
fsrs: fsrsCards,
|
||
mistakes: quizMistakes,
|
||
stats: {
|
||
maxStreak: quizMaxStreak
|
||
}
|
||
};
|
||
}
|
||
|
||
function encodeSyncPayload(payload) {
|
||
const jsonStr = JSON.stringify(payload);
|
||
return btoa(unescape(encodeURIComponent(jsonStr)));
|
||
}
|
||
|
||
function decodeSyncPayload(str) {
|
||
try {
|
||
const jsonStr = decodeURIComponent(escape(atob(str)));
|
||
return JSON.parse(jsonStr);
|
||
} catch (err) {
|
||
console.error('Decoding sync payload failed:', err);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function openSyncModal() {
|
||
const payload = getSyncPayload();
|
||
const token = encodeSyncPayload(payload);
|
||
|
||
const isFsrs = cardAlgo === 'fsrs';
|
||
let masteredDisplay = '';
|
||
if (isFsrs) {
|
||
const fsrsItems = Object.values(fsrsCards);
|
||
const stableCount = fsrsItems.filter(c => c && c.reps > 0 && c.s >= 14).length;
|
||
masteredDisplay = `${stableCount} / ${STILMITTEL_DATA.length} stabil`;
|
||
} else {
|
||
const masteredCount = STILMITTEL_DATA.filter(item => (cardLevels[item.id] || 1) >= 5).length;
|
||
masteredDisplay = `${masteredCount} / ${STILMITTEL_DATA.length} (Kasten 5)`;
|
||
}
|
||
|
||
document.getElementById('syncStatMastered').innerText = masteredDisplay;
|
||
document.getElementById('syncStatMistakes').innerText = `${quizMistakes.length}`;
|
||
document.getElementById('syncStatStreak').innerText = `${quizMaxStreak}`;
|
||
|
||
document.getElementById('syncExportText').value = token;
|
||
|
||
// Render QR Code
|
||
const syncUrl = `${window.location.origin}${window.location.pathname}#sync=${token}`;
|
||
renderQrCode(syncUrl);
|
||
|
||
switchSyncTab('qr');
|
||
playTone('flip');
|
||
document.getElementById('syncModal').classList.remove('hidden');
|
||
}
|
||
|
||
function closeSyncModal() {
|
||
document.getElementById('syncModal').classList.add('hidden');
|
||
}
|
||
|
||
function switchSyncTab(tab) {
|
||
['qr', 'json', 'code'].forEach(t => {
|
||
const sec = document.getElementById(`syncSection-${t}`);
|
||
const btn = document.getElementById(`syncTabBtn-${t}`);
|
||
if (sec) sec.classList.toggle('hidden', t !== tab);
|
||
if (btn) {
|
||
if (t === tab) {
|
||
btn.className = 'flex-1 py-1.5 rounded-lg bg-white text-indigo-700 shadow-sm transition font-bold';
|
||
} else {
|
||
btn.className = 'flex-1 py-1.5 rounded-lg text-slate-600 hover:text-slate-900 transition font-semibold';
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
function renderQrCode(url) {
|
||
const container = document.getElementById('syncQrContainer');
|
||
if (!container) return;
|
||
try {
|
||
if (typeof qrcode === 'function') {
|
||
const qr = qrcode(0, 'M');
|
||
qr.addData(url);
|
||
qr.make();
|
||
container.innerHTML = qr.createImgTag(5, 8);
|
||
const img = container.querySelector('img');
|
||
if (img) {
|
||
img.className = 'rounded-xl max-w-full h-auto mx-auto shadow-sm';
|
||
img.alt = 'RhetorikMaster Mobil Sync QR Code';
|
||
}
|
||
} else {
|
||
container.innerHTML = '<p class="text-xs text-slate-400">QR-Code Engine lädt...</p>';
|
||
}
|
||
} catch (err) {
|
||
console.error('QR creation error:', err);
|
||
container.innerHTML = '<p class="text-xs text-rose-500">QR-Code konnte nicht generiert werden.</p>';
|
||
}
|
||
}
|
||
|
||
function copySyncUrl() {
|
||
const payload = getSyncPayload();
|
||
const token = encodeSyncPayload(payload);
|
||
const syncUrl = `${window.location.origin}${window.location.pathname}#sync=${token}`;
|
||
|
||
navigator.clipboard.writeText(syncUrl).then(() => {
|
||
const btnText = document.getElementById('copySyncUrlBtnText');
|
||
if (btnText) btnText.innerText = 'Link kopiert!';
|
||
showToast('Sync-Link kopiert! Auf anderem Gerät öffnen.', 'fa-link');
|
||
setTimeout(() => {
|
||
if (btnText) btnText.innerText = 'Sync-Link kopieren';
|
||
}, 2000);
|
||
}).catch(() => {
|
||
showToast('Kopieren fehlgeschlagen', 'fa-triangle-exclamation');
|
||
});
|
||
}
|
||
|
||
function copySyncToken() {
|
||
const text = document.getElementById('syncExportText').value;
|
||
navigator.clipboard.writeText(text).then(() => {
|
||
showToast('Sync-Code in die Zwischenablage kopiert!', 'fa-copy');
|
||
});
|
||
}
|
||
|
||
function exportJsonFile() {
|
||
const payload = getSyncPayload();
|
||
const dataStr = 'data:text/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(payload, null, 2));
|
||
const downloadAnchor = document.createElement('a');
|
||
const dateStr = new Date().toISOString().slice(0, 10);
|
||
downloadAnchor.setAttribute('href', dataStr);
|
||
downloadAnchor.setAttribute('download', `rhetorikmaster_backup_${dateStr}.json`);
|
||
document.body.appendChild(downloadAnchor);
|
||
downloadAnchor.click();
|
||
downloadAnchor.remove();
|
||
showToast('Backup als JSON-Datei exportiert!', 'fa-download');
|
||
}
|
||
|
||
function importJsonFile(e) {
|
||
const file = e.target.files && e.target.files[0];
|
||
if (!file) return;
|
||
|
||
const reader = new FileReader();
|
||
reader.onload = function(evt) {
|
||
try {
|
||
const payload = JSON.parse(evt.target.result);
|
||
applySyncData(payload);
|
||
closeSyncModal();
|
||
closeAchievementsModal();
|
||
showToast('Lernstand aus JSON-Datei importiert!', 'fa-circle-check');
|
||
} catch (err) {
|
||
alert('Ungültiges Backup-Format in der ausgewählten JSON-Datei.');
|
||
}
|
||
};
|
||
reader.readAsText(file);
|
||
e.target.value = '';
|
||
}
|
||
|
||
function applyManualSyncToken() {
|
||
const token = document.getElementById('syncImportText').value.trim();
|
||
if (!token) return;
|
||
const payload = decodeSyncPayload(token);
|
||
if (!payload || !payload.levels) {
|
||
alert('Ungültiger Sync-Code. Bitte prüfe die Eingabe.');
|
||
return;
|
||
}
|
||
applySyncData(payload);
|
||
closeSyncModal();
|
||
closeAchievementsModal();
|
||
document.getElementById('syncImportText').value = '';
|
||
showToast('Lernstand erfolgreich aktualisiert!', 'fa-circle-check');
|
||
}
|
||
|
||
function applySyncData(payload) {
|
||
if (!payload) return;
|
||
if (payload.levels && typeof payload.levels === 'object') {
|
||
cardLevels = payload.levels;
|
||
localStorage.setItem('rhetorik_leitner_levels', JSON.stringify(cardLevels));
|
||
}
|
||
if (payload.fsrs && typeof payload.fsrs === 'object') {
|
||
fsrsCards = payload.fsrs;
|
||
localStorage.setItem('rhetorik_fsrs_cards', JSON.stringify(fsrsCards));
|
||
}
|
||
if (payload.algo && (payload.algo === 'leitner' || payload.algo === 'fsrs')) {
|
||
cardAlgo = payload.algo;
|
||
localStorage.setItem('rhetorik_card_algo', cardAlgo);
|
||
}
|
||
if (Array.isArray(payload.mistakes)) {
|
||
quizMistakes = payload.mistakes;
|
||
localStorage.setItem('rhetorik_quiz_mistakes', JSON.stringify(quizMistakes));
|
||
}
|
||
if (payload.stats && typeof payload.stats.maxStreak === 'number') {
|
||
quizMaxStreak = Math.max(quizMaxStreak, payload.stats.maxStreak);
|
||
}
|
||
|
||
setSpacedRepetitionAlgo(cardAlgo);
|
||
updateMasteredHeader();
|
||
renderCurrentCard();
|
||
showToast('Lernstand erfolgreich synchronisiert!', 'fa-arrows-rotate');
|
||
}
|
||
|
||
function checkUrlSync() {
|
||
const hash = window.location.hash;
|
||
if (hash && hash.startsWith('#sync=')) {
|
||
const token = hash.replace('#sync=', '').trim();
|
||
const payload = decodeSyncPayload(token);
|
||
if (payload && (payload.levels || payload.fsrs)) {
|
||
pendingIncomingSync = payload;
|
||
let masteredCount = 0;
|
||
if (payload.algo === 'fsrs' && payload.fsrs) {
|
||
masteredCount = Object.values(payload.fsrs).filter(c => c && c.reps > 0 && c.s >= 14).length;
|
||
} else if (payload.levels) {
|
||
masteredCount = STILMITTEL_DATA.filter(item => (payload.levels[item.id] || 1) >= 5).length;
|
||
}
|
||
document.getElementById('incomingSyncMastered').innerText = `${masteredCount} von ${STILMITTEL_DATA.length}`;
|
||
document.getElementById('incomingSyncMistakes').innerText = `${(payload.mistakes || []).length}`;
|
||
document.getElementById('incomingSyncStreak').innerText = `${(payload.stats && payload.stats.maxStreak) || 0}`;
|
||
document.getElementById('incomingSyncModal').classList.remove('hidden');
|
||
}
|
||
}
|
||
}
|
||
|
||
function confirmIncomingSync() {
|
||
if (pendingIncomingSync) {
|
||
applySyncData(pendingIncomingSync);
|
||
pendingIncomingSync = null;
|
||
}
|
||
document.getElementById('incomingSyncModal').classList.add('hidden');
|
||
if (window.history && window.history.replaceState) {
|
||
window.history.replaceState(null, '', window.location.pathname + window.location.search);
|
||
}
|
||
}
|
||
|
||
function cancelIncomingSync() {
|
||
pendingIncomingSync = null;
|
||
document.getElementById('incomingSyncModal').classList.add('hidden');
|
||
if (window.history && window.history.replaceState) {
|
||
window.history.replaceState(null, '', window.location.pathname + window.location.search);
|
||
}
|
||
}
|
||
|
||
|
||
// ==========================================
|
||
// 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() {
|
||
function isMasteredCard(item) {
|
||
if ((cardLevels[item.id] || 1) >= 5) return true;
|
||
const c = fsrsCards[item.id];
|
||
return !!(c && c.reps >= 2 && c.s >= 14);
|
||
}
|
||
const masteredCount = STILMITTEL_DATA.filter(item => isMasteredCard(item)).length;
|
||
const klangMastered = STILMITTEL_DATA.filter(item => item.category === 'klang' && isMasteredCard(item)).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
|
||
window.addEventListener('keydown', (e) => {
|
||
if (document.activeElement && document.activeElement.tagName === 'INPUT') {
|
||
if (e.key === 'Escape') document.activeElement.blur();
|
||
return;
|
||
}
|
||
|
||
if (e.key === 'm' || e.key === 'M') {
|
||
toggleSound();
|
||
return;
|
||
}
|
||
|
||
if (e.key === 'p' || e.key === 'P') {
|
||
printCheatsheet();
|
||
return;
|
||
}
|
||
|
||
if (e.key === '/') {
|
||
e.preventDefault();
|
||
switchTab('glossary');
|
||
const input = document.getElementById('glossarySearchInput');
|
||
if (input) input.focus();
|
||
return;
|
||
}
|
||
|
||
if (e.key === 'Escape') {
|
||
closeDetailModal();
|
||
closeSyncModal();
|
||
closeAchievementsModal();
|
||
cancelIncomingSync();
|
||
return;
|
||
}
|
||
|
||
if (currentTabIndex === 'cards') {
|
||
if (e.code === 'Space') {
|
||
e.preventDefault();
|
||
flipCard();
|
||
} else if (e.code === 'ArrowRight') {
|
||
nextCard();
|
||
} else if (e.code === 'ArrowLeft') {
|
||
prevCard();
|
||
} else if (cardAlgo === 'fsrs') {
|
||
if (e.key === '1') rateFsrsCard(1);
|
||
else if (e.key === '2') rateFsrsCard(2);
|
||
else if (e.key === '3') rateFsrsCard(3);
|
||
else if (e.key === '4') rateFsrsCard(4);
|
||
} else {
|
||
if (e.key === '1') markCardMastered(false);
|
||
else if (e.key === '2') markCardMastered(true);
|
||
}
|
||
} else if (currentTabIndex === 'quiz') {
|
||
if (['1', '2', '3', '4'].includes(e.key)) {
|
||
const idx = parseInt(e.key) - 1;
|
||
const btn = document.getElementById(`quiz-opt-btn-${idx}`);
|
||
if (btn && !isQuestionAnswered) btn.click();
|
||
} else if (e.key === 'Enter' || e.code === 'Space') {
|
||
if (isQuestionAnswered) {
|
||
e.preventDefault();
|
||
nextQuizQuestion();
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
document.getElementById('detailModal').addEventListener('click', (e) => {
|
||
if (e.target.id === 'detailModal') closeDetailModal();
|
||
});
|
||
|
||
// PWA Service Worker Registration & Live Update Detection
|
||
let deferredPrompt = null;
|
||
if ('serviceWorker' in navigator) {
|
||
window.addEventListener('load', () => {
|
||
navigator.serviceWorker.register('/sw.js').then(reg => {
|
||
reg.update();
|
||
}).catch(err => {
|
||
console.warn('SW registration failed:', err);
|
||
});
|
||
});
|
||
|
||
let refreshing = false;
|
||
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
||
if (!refreshing) {
|
||
refreshing = true;
|
||
window.location.reload();
|
||
}
|
||
});
|
||
}
|
||
|
||
function forceAppUpdate() {
|
||
if ('caches' in window) {
|
||
caches.keys().then(names => Promise.all(names.map(name => caches.delete(name)))).then(() => {
|
||
if ('serviceWorker' in navigator) {
|
||
navigator.serviceWorker.getRegistrations().then(regs => {
|
||
for (let reg of regs) reg.unregister();
|
||
window.location.reload(true);
|
||
});
|
||
} else {
|
||
window.location.reload(true);
|
||
}
|
||
});
|
||
} else {
|
||
window.location.reload(true);
|
||
}
|
||
}
|
||
|
||
window.addEventListener('beforeinstallprompt', (e) => {
|
||
e.preventDefault();
|
||
deferredPrompt = e;
|
||
const installBtn = document.getElementById('pwaInstallBtn');
|
||
if (installBtn) {
|
||
installBtn.classList.remove('hidden');
|
||
installBtn.classList.add('flex');
|
||
}
|
||
});
|
||
|
||
function installPWA() {
|
||
if (deferredPrompt) {
|
||
deferredPrompt.prompt();
|
||
deferredPrompt.userChoice.then(() => {
|
||
deferredPrompt = null;
|
||
const installBtn = document.getElementById('pwaInstallBtn');
|
||
if (installBtn) installBtn.classList.add('hidden');
|
||
});
|
||
}
|
||
}
|
||
|
||
window.onload = function() {
|
||
updateSoundUI();
|
||
setSpacedRepetitionAlgo(cardAlgo);
|
||
updateMasteredHeader();
|
||
renderPrintCheatsheet();
|
||
updateGamificationHeader();
|
||
checkAchievements();
|
||
checkUrlSync();
|
||
};
|
||
</script>
|
||
</body>
|
||
</html> |