Add Wortbaukasten, Preposition+Case trainer, and Listening Dictation modules
This commit is contained in:
1 parent
e636350940
commit
5f013dbdfe
5 files changed
+1646
-23
No files matched your search
@@ -25,6 +25,26 @@ class WortKraftApp {
|
||||
this.spotlightWords = [];
|
||||
this.pendingTranslations = new Set();
|
||||
|
||||
// 1) Wortbaukasten (Compound & Prefix Lab) state
|
||||
this.builderSubmode = "prefixes"; // 'prefixes' | 'compounds' | 'quiz'
|
||||
this.selectedVerbRoot = "nehmen";
|
||||
this.builderQuizIndex = 0;
|
||||
this.builderQuizResult = null;
|
||||
|
||||
// 2) Verben mit Präposition (Rektion & Da-/Wo-) state
|
||||
this.prepSubmode = "drill"; // 'drill' | 'dawo' | 'reference'
|
||||
this.prepIndex = 0;
|
||||
this.prepResult = null;
|
||||
this.daWoIndex = 0;
|
||||
this.daWoResult = null;
|
||||
|
||||
// 3) Hörtraining (Listening Dictation & Shadowing) state
|
||||
this.listeningIndex = 0;
|
||||
this.listeningShowHint = false;
|
||||
this.listeningResult = null;
|
||||
this.speechResult = null;
|
||||
this.isRecordingMic = false;
|
||||
|
||||
this.loadState();
|
||||
this.hoverTranslateEnabled = this.settings.hoverTranslateEnabled !== false;
|
||||
this.initScenarioStates();
|
||||
@@ -477,6 +497,12 @@ class WortKraftApp {
|
||||
} else if (viewName === "trainer") {
|
||||
if (!this.currentCard) this.pickNextCard();
|
||||
this.renderTrainerStage();
|
||||
} else if (viewName === "builder") {
|
||||
this.renderBuilderStage();
|
||||
} else if (viewName === "prepositions") {
|
||||
this.renderPrepositionsStage();
|
||||
} else if (viewName === "listening") {
|
||||
this.renderListeningStage();
|
||||
} else if (viewName === "reader") {
|
||||
this.renderReaderSidebar();
|
||||
this.renderReaderMain();
|
||||
@@ -1924,6 +1950,787 @@ class WortKraftApp {
|
||||
.replace(/\n/g, " ");
|
||||
}
|
||||
|
||||
// === 1) WORTBAUKASTEN (COMPOUND WORD & PREFIX LAB) ===
|
||||
setBuilderSubmode(submode) {
|
||||
this.builderSubmode = submode;
|
||||
this.builderQuizResult = null;
|
||||
["prefixes", "compounds", "quiz"].forEach(m => {
|
||||
const btn = document.getElementById(`builder-tab-${m}`);
|
||||
if (btn) btn.classList.toggle("active", m === submode);
|
||||
});
|
||||
this.renderBuilderStage();
|
||||
}
|
||||
|
||||
selectVerbRoot(rootName) {
|
||||
this.selectedVerbRoot = rootName;
|
||||
this.renderBuilderStage();
|
||||
}
|
||||
|
||||
saveLabItemToDeck(word, article, plural, english, exampleDe, exampleEn, note) {
|
||||
const existing = this.vocab.find(v => v.word.toLowerCase() === word.toLowerCase());
|
||||
if (existing) {
|
||||
existing.srs.dueDate = Date.now() - 1000;
|
||||
this.saveState();
|
||||
this.showToast(`⚡ „${word}“ wurde im SRS-Trainer priorisiert!`);
|
||||
return;
|
||||
}
|
||||
const newEntry = this.createVocabEntry({
|
||||
id: "lab_" + Date.now() + "_" + Math.floor(Math.random() * 1000),
|
||||
word,
|
||||
article: article || null,
|
||||
plural: plural || null,
|
||||
pos: article ? "noun" : "verb",
|
||||
level: "A2/B1",
|
||||
category: "Everyday & Bureaucracy",
|
||||
english,
|
||||
breakdown: note,
|
||||
forms: plural,
|
||||
exampleDe,
|
||||
exampleEn: exampleEn || "",
|
||||
clozeSentence: exampleDe.replace(new RegExp(word, "i"), "___"),
|
||||
clozeAnswer: word,
|
||||
usageTip: note
|
||||
});
|
||||
this.vocab.unshift(newEntry);
|
||||
this.saveState();
|
||||
this.showToast(`➕ „${word}“ zum SRS-Trainer hinzugefügt!`);
|
||||
}
|
||||
|
||||
getBuilderQuizItems() {
|
||||
const data = window.WORD_BUILDER_DATA;
|
||||
if (!data) return [];
|
||||
const items = [];
|
||||
data.verbRoots.forEach(r => {
|
||||
r.derivatives.forEach(d => {
|
||||
items.push({
|
||||
type: "prefix",
|
||||
root: r.root,
|
||||
word: d.word,
|
||||
separable: d.separable,
|
||||
english: d.english,
|
||||
promptDe: d.quizCloze,
|
||||
answer: d.quizAnswer,
|
||||
explanation: `${d.word} (${d.forms}) — ${d.logic}`
|
||||
});
|
||||
});
|
||||
});
|
||||
data.compounds.forEach(c => {
|
||||
items.push({
|
||||
type: "compound",
|
||||
compound: c.compound,
|
||||
finalArticle: c.finalArticle,
|
||||
english: c.english,
|
||||
parts: c.parts,
|
||||
explanation: c.ruleNote,
|
||||
exampleDe: c.exampleDe
|
||||
});
|
||||
});
|
||||
return items;
|
||||
}
|
||||
|
||||
checkBuilderQuizAnswer(userVal) {
|
||||
const items = this.getBuilderQuizItems();
|
||||
const item = items[this.builderQuizIndex % items.length];
|
||||
if (!item) return;
|
||||
|
||||
this.stats.totalChecks++;
|
||||
if (item.type === "prefix") {
|
||||
const isCorrect = this.normalizeGerman(userVal) === this.normalizeGerman(item.answer);
|
||||
if (isCorrect) this.stats.correctChecks++;
|
||||
this.builderQuizResult = {
|
||||
isCorrect,
|
||||
message: isCorrect
|
||||
? `✅ Richtig! „${item.answer}“ (${item.word} — ${item.separable ? "trennbar" : "untrennbar"}).`
|
||||
: `❌ Fast! Gesucht war: „${item.answer}“ (${item.word}).`
|
||||
};
|
||||
this.speak(item.promptDe.replace("___", item.answer));
|
||||
} else {
|
||||
const isCorrect = userVal === item.finalArticle;
|
||||
if (isCorrect) this.stats.correctChecks++;
|
||||
this.builderQuizResult = {
|
||||
isCorrect,
|
||||
message: isCorrect
|
||||
? `✅ Perfekt! Das letzte Wort bestimmt den Artikel: ${item.finalArticle} ${item.compound}!`
|
||||
: `❌ Nicht ganz: Das letzte Wort in „${item.compound}“ bestimmt den Artikel → ${item.finalArticle} ${item.compound}!`
|
||||
};
|
||||
this.speak(`${item.finalArticle} ${item.compound}`);
|
||||
}
|
||||
this.saveState();
|
||||
this.renderBuilderStage();
|
||||
}
|
||||
|
||||
nextBuilderQuiz() {
|
||||
const items = this.getBuilderQuizItems();
|
||||
this.builderQuizIndex = (this.builderQuizIndex + 1) % items.length;
|
||||
this.builderQuizResult = null;
|
||||
this.renderBuilderStage();
|
||||
}
|
||||
|
||||
renderBuilderStage() {
|
||||
const container = document.getElementById("builder-stage-container");
|
||||
const data = window.WORD_BUILDER_DATA;
|
||||
if (!container || !data) return;
|
||||
|
||||
if (this.builderSubmode === "prefixes") {
|
||||
const activeRoot = data.verbRoots.find(r => r.root === this.selectedVerbRoot) || data.verbRoots[0];
|
||||
const rootPills = data.verbRoots
|
||||
.map(
|
||||
r => `
|
||||
<button class="btn ${r.root === activeRoot.root ? "btn-primary" : "btn-secondary"}" onclick="app.selectVerbRoot('${r.root}')">
|
||||
Stamm: <strong>${r.root}</strong>
|
||||
</button>
|
||||
`
|
||||
)
|
||||
.join("");
|
||||
|
||||
const cardsHtml = activeRoot.derivatives
|
||||
.map(
|
||||
d => `
|
||||
<div class="prefix-card">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; gap: 0.5rem;">
|
||||
<div>
|
||||
<span class="sep-pill ${d.separable ? "sep-yes" : "sep-no"}">
|
||||
${d.separable ? "✂️ Trennbar (mit -ge-)" : "🔒 Untrennbar (ohne -ge-)"}
|
||||
</span>
|
||||
</div>
|
||||
<div style="display: flex; gap: 0.35rem;">
|
||||
<button class="icon-btn" style="width: 30px; height: 30px; font-size: 0.8rem;" onclick="app.speak('${this.escapeJs(d.word + ". " + d.exampleDe)}')" title="Anhören">🔊</button>
|
||||
<button class="icon-btn" style="width: 30px; height: 30px; font-size: 0.8rem;" onclick="app.saveLabItemToDeck('${this.escapeJs(d.word)}', null, '${this.escapeJs(d.forms)}', '${this.escapeJs(d.english)}', '${this.escapeJs(d.exampleDe)}', '${this.escapeJs(d.exampleEn)}', '${this.escapeJs(d.logic)}')" title="Ins SRS-Deck speichern">➕</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="font-size: 1.2rem; font-weight: 800; margin-top: 0.2rem;">
|
||||
<span style="color: var(--primary);">${d.prefix}</span>${activeRoot.root} → ${d.word}
|
||||
</div>
|
||||
<div style="font-size: 0.8rem; color: var(--text-muted); font-weight: 600;">${d.forms}</div>
|
||||
<div style="font-weight: 700; color: var(--text-main);">${d.english}</div>
|
||||
<div style="font-size: 0.82rem; color: var(--text-secondary);">💡 <strong>Logik:</strong> ${d.logic}</div>
|
||||
<div style="font-size: 0.88rem; background: var(--bg-main); padding: 0.6rem 0.75rem; border-radius: 8px; margin-top: 0.25rem;">
|
||||
„${this.tokenizeSentenceForHover(d.exampleDe)}“
|
||||
<div style="font-size: 0.78rem; color: var(--text-muted); margin-top: 0.2rem;">${d.exampleEn}</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
.join("");
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="panel-card">
|
||||
<div style="display: flex; justify-content: space-between; align-items: flex-start; flex-wrap: wrap; gap: 1rem; margin-bottom: 1rem;">
|
||||
<div>
|
||||
<h2 style="font-size: 1.35rem; font-weight: 800;">🔀 Verb-Stämme & Präfix-Labor</h2>
|
||||
<p style="font-size: 0.9rem; color: var(--text-secondary);">
|
||||
Ein einziger deutscher Verbstamm bildet durch Vorsilben (Präfixe) dutzende neue Wörter.
|
||||
<strong>Trennbare</strong> Präfixe (an-, ab-, auf-, aus-, mit-, vor-, teil-) sind betont und bekommen im Perfekt <code>-ge-</code> in der Mitte.
|
||||
<strong>Untrennbare</strong> Präfixe (be-, ver-, ent-, er-, über-, unter-) bekommen <u>kein</u> <code>-ge-</code>!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; gap: 0.6rem; flex-wrap: wrap; margin-bottom: 1rem;">
|
||||
${rootPills}
|
||||
</div>
|
||||
<div style="padding: 0.75rem 1rem; background: var(--bg-subtle); border-radius: 10px; font-weight: 600; font-size: 0.92rem;">
|
||||
Grundverb: <strong>${activeRoot.root}</strong> — ${activeRoot.baseMeaning}
|
||||
</div>
|
||||
<div class="prefix-grid">
|
||||
${cardsHtml}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else if (this.builderSubmode === "compounds") {
|
||||
const compoundsHtml = data.compounds
|
||||
.map(c => {
|
||||
const blocksHtml = c.parts
|
||||
.map((part, idx) => {
|
||||
const isLast = idx === c.parts.length - 1;
|
||||
return `
|
||||
${idx > 0 ? `<div class="compound-plus">+</div>` : ""}
|
||||
<div class="compound-block ${isLast ? "head-word" : ""}">
|
||||
<div style="display: flex; align-items: center; gap: 0.35rem;">
|
||||
${part.article ? this.getGenderBadgeHtml(part.article) : ""}
|
||||
<strong style="font-size: 1.02rem;">${part.word}</strong>
|
||||
</div>
|
||||
<span style="font-size: 0.78rem; color: var(--text-secondary);">${part.meaning}</span>
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
return `
|
||||
<div class="panel-card" style="margin-bottom: 1rem;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.75rem;">
|
||||
<div style="display: flex; align-items: center; gap: 0.55rem; flex-wrap: wrap;">
|
||||
${this.getGenderBadgeHtml(c.finalArticle)}
|
||||
<strong style="font-size: 1.35rem;">${this.tokenizeSentenceForHover(c.compound)}</strong>
|
||||
<span style="font-size: 0.85rem; color: var(--text-muted);">(${c.plural})</span>
|
||||
<span style="font-weight: 600; color: var(--text-secondary);">— ${c.english}</span>
|
||||
</div>
|
||||
<div style="display: flex; gap: 0.45rem;">
|
||||
<button class="btn btn-secondary" style="padding: 0.35rem 0.75rem; font-size: 0.82rem;" onclick="app.speak('${this.escapeJs(c.finalArticle + " " + c.compound + ". " + c.exampleDe)}')">🔊 Anhören</button>
|
||||
<button class="btn btn-primary" style="padding: 0.35rem 0.75rem; font-size: 0.82rem;" onclick="app.saveLabItemToDeck('${this.escapeJs(c.compound)}', '${c.finalArticle}', '${this.escapeJs(c.plural)}', '${this.escapeJs(c.english)}', '${this.escapeJs(c.exampleDe)}', '', '${this.escapeJs(c.ruleNote)}')">➕ Ins SRS-Deck</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="compound-blocks-row">
|
||||
${blocksHtml}
|
||||
</div>
|
||||
<div style="font-size: 0.86rem; color: var(--text-secondary); margin-bottom: 0.45rem;">
|
||||
🎯 <strong>Artikel-Regel:</strong> ${c.ruleNote}
|
||||
</div>
|
||||
<div style="font-size: 0.9rem; background: var(--bg-main); padding: 0.6rem 0.85rem; border-radius: 8px;">
|
||||
„${this.tokenizeSentenceForHover(c.exampleDe)}“
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
container.innerHTML = `
|
||||
<div style="margin-bottom: 1.25rem;">
|
||||
<h2 style="font-size: 1.35rem; font-weight: 800;">🧱 Komposita-Knacker: Keine Angst vor langen deutschen Wörtern!</h2>
|
||||
<p style="font-size: 0.92rem; color: var(--text-secondary);">
|
||||
Goldene Regel: Egal wie lang ein zusammengesetztes Nomen ist — <strong>das allerletzte Baustein-Wort (gelb umrandet) bestimmt immer 100% den Artikel (der/die/das)!</strong>
|
||||
</p>
|
||||
</div>
|
||||
${compoundsHtml}
|
||||
`;
|
||||
} else {
|
||||
// Quiz submode
|
||||
const items = this.getBuilderQuizItems();
|
||||
const item = items[this.builderQuizIndex % items.length];
|
||||
|
||||
let quizBody = "";
|
||||
if (item.type === "prefix") {
|
||||
quizBody = `
|
||||
<div class="prompt-instruction">🔀 Präfix-Verb im Kontext • Stamm: „${item.root}“</div>
|
||||
<div class="prompt-main">${item.english}</div>
|
||||
<div class="cloze-box">
|
||||
<div class="cloze-de">${this.tokenizeSentenceForHover(item.promptDe)}</div>
|
||||
</div>
|
||||
<form onsubmit="event.preventDefault(); app.checkBuilderQuizAnswer(document.getElementById('builder-quiz-input').value)">
|
||||
<div class="answer-input-row">
|
||||
<input type="text" id="builder-quiz-input" class="german-input" placeholder="Fehlende Verbform eingeben (z.B. ${item.answer.slice(0, 3)}...)" autocomplete="off" />
|
||||
<button type="submit" class="btn btn-primary">Prüfen</button>
|
||||
<button type="button" class="btn btn-secondary" onclick="app.nextBuilderQuiz()">Nächste Frage →</button>
|
||||
</div>
|
||||
${this.renderUmlautBar("builder-quiz-input")}
|
||||
</form>
|
||||
`;
|
||||
} else {
|
||||
const partsLine = item.parts.map(p => `${p.article ? p.article + " " : ""}${p.word}`).join(" + ");
|
||||
quizBody = `
|
||||
<div class="prompt-instruction">🧱 Komposita-Artikel-Challenge • Welcher Artikel gewinnt?</div>
|
||||
<div class="prompt-main">___ ${item.compound}</div>
|
||||
<div style="font-size: 0.95rem; color: var(--text-secondary); margin-bottom: 1rem;">
|
||||
Bausteine: <strong>${partsLine}</strong> (${item.english})
|
||||
</div>
|
||||
<div class="gender-choice-grid" style="grid-template-columns: repeat(3, 1fr);">
|
||||
<button class="gender-choice-btn gender-der" onclick="app.checkBuilderQuizAnswer('der')">der</button>
|
||||
<button class="gender-choice-btn gender-die" onclick="app.checkBuilderQuizAnswer('die')">die</button>
|
||||
<button class="gender-choice-btn gender-das" onclick="app.checkBuilderQuizAnswer('das')">das</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="srs-card-stage">
|
||||
<div class="card-meta-top">
|
||||
<span class="category-tag">Wortbaukasten-Quiz • Frage ${(this.builderQuizIndex % items.length) + 1} / ${items.length}</span>
|
||||
</div>
|
||||
${quizBody}
|
||||
${
|
||||
this.builderQuizResult
|
||||
? `
|
||||
<div class="feedback-panel ${this.builderQuizResult.isCorrect ? "correct" : "incorrect"}">
|
||||
<div style="font-weight: 700; margin-bottom: 0.45rem;">${this.builderQuizResult.message}</div>
|
||||
<div style="font-size: 0.9rem; color: var(--text-secondary);">${item.explanation}</div>
|
||||
<div style="margin-top: 0.85rem;">
|
||||
<button class="btn btn-primary" onclick="app.nextBuilderQuiz()">Weiter zur nächsten Aufgabe →</button>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// === 2) VERBEN MIT PRÄPOSITION & KASUS (REKTION & DA-/WO-) ===
|
||||
setPrepSubmode(submode) {
|
||||
this.prepSubmode = submode;
|
||||
this.prepResult = null;
|
||||
this.daWoResult = null;
|
||||
["drill", "dawo", "reference"].forEach(m => {
|
||||
const btn = document.getElementById(`prep-tab-${m}`);
|
||||
if (btn) btn.classList.toggle("active", m === submode);
|
||||
});
|
||||
this.renderPrepositionsStage();
|
||||
}
|
||||
|
||||
checkPrepDrillChoice(chosenOption) {
|
||||
const drills = window.PREPOSITION_DRILLS || [];
|
||||
const d = drills[this.prepIndex % drills.length];
|
||||
if (!d) return;
|
||||
|
||||
this.stats.totalChecks++;
|
||||
const cleanChosen = chosenOption.split(" (")[0].trim().toLowerCase();
|
||||
const isCorrect =
|
||||
cleanChosen === d.correctCombo.toLowerCase() ||
|
||||
(d.acceptedAnswers || []).some(a => a.toLowerCase() === cleanChosen);
|
||||
|
||||
if (isCorrect) this.stats.correctChecks++;
|
||||
|
||||
const fullSentence = `${d.sentenceBefore} ${d.correctCombo} ${d.sentenceAfter}`;
|
||||
this.prepResult = {
|
||||
isCorrect,
|
||||
fullSentence,
|
||||
message: isCorrect
|
||||
? `✅ Perfekt! „${d.verb} + ${d.preposition} + ${d.grammaticalCase}“ → „${d.correctCombo}“.`
|
||||
: `❌ Fast! Du hast „${chosenOption}“ gewählt, aber richtig ist: „${d.correctCombo}“ (${d.verb} + ${d.preposition} + ${d.grammaticalCase}).`
|
||||
};
|
||||
this.saveState();
|
||||
this.speak(fullSentence);
|
||||
this.renderPrepositionsStage();
|
||||
}
|
||||
|
||||
nextPrepDrill() {
|
||||
const drills = window.PREPOSITION_DRILLS || [];
|
||||
this.prepIndex = (this.prepIndex + 1) % drills.length;
|
||||
this.prepResult = null;
|
||||
this.renderPrepositionsStage();
|
||||
}
|
||||
|
||||
checkDaWoAnswer(chosenWord) {
|
||||
const drills = window.PREPOSITION_DRILLS || [];
|
||||
const d = drills[this.daWoIndex % drills.length];
|
||||
if (!d) return;
|
||||
|
||||
this.stats.totalChecks++;
|
||||
const isCorrect = chosenWord.toLowerCase() === d.daForm.toLowerCase();
|
||||
if (isCorrect) this.stats.correctChecks++;
|
||||
|
||||
this.daWoResult = {
|
||||
isCorrect,
|
||||
message: isCorrect
|
||||
? `✅ Genau richtig! da(r) + ${d.preposition} = „${d.daForm}“ (Fragewort: „${d.woForm}“).`
|
||||
: `❌ Nicht ganz: Zu „${d.verb} + ${d.preposition}“ gehört „${d.daForm}“ (und als Frage: „${d.woForm}“).`
|
||||
};
|
||||
this.saveState();
|
||||
this.speak(d.daExampleDe);
|
||||
this.renderPrepositionsStage();
|
||||
}
|
||||
|
||||
nextDaWoDrill() {
|
||||
const drills = window.PREPOSITION_DRILLS || [];
|
||||
this.daWoIndex = (this.daWoIndex + 1) % drills.length;
|
||||
this.daWoResult = null;
|
||||
this.renderPrepositionsStage();
|
||||
}
|
||||
|
||||
renderPrepositionsStage() {
|
||||
const container = document.getElementById("prepositions-stage-container");
|
||||
const drills = window.PREPOSITION_DRILLS || [];
|
||||
if (!container || drills.length === 0) return;
|
||||
|
||||
if (this.prepSubmode === "drill") {
|
||||
const d = drills[this.prepIndex % drills.length];
|
||||
const optionsHtml = d.options
|
||||
.map(
|
||||
opt => `
|
||||
<button
|
||||
class="btn btn-secondary"
|
||||
style="justify-content: center; font-size: 1.05rem; padding: 0.85rem;"
|
||||
onclick="app.checkPrepDrillChoice('${this.escapeJs(opt)}')"
|
||||
>
|
||||
${opt}
|
||||
</button>
|
||||
`
|
||||
)
|
||||
.join("");
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="srs-card-stage">
|
||||
<div class="card-meta-top">
|
||||
<span class="category-tag">Präposition & Kasus • ${(this.prepIndex % drills.length) + 1} / ${drills.length}</span>
|
||||
<span class="${d.grammaticalCase === "Akkusativ" ? "case-badge-akk" : "case-badge-dat"}">
|
||||
Verb: ${d.verb} (${d.english})
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="prompt-instruction">🎯 Wähle die richtige Präposition + Artikel im richtigen Fall (Akkusativ / Dativ)</div>
|
||||
<div class="cloze-box" style="margin: 1rem 0;">
|
||||
<div class="cloze-de">
|
||||
${this.tokenizeSentenceForHover(d.sentenceBefore)}
|
||||
<span style="color:var(--primary);border-bottom:2px solid var(--primary);padding:0 0.5rem;font-weight:800;">[ ___ ]</span>
|
||||
${this.tokenizeSentenceForHover(d.sentenceAfter)}
|
||||
</div>
|
||||
<div class="cloze-en" style="margin-top: 0.35rem;">
|
||||
Nomen-Hinweis: <strong>${d.nounHint}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.75rem; margin: 1.25rem 0;">
|
||||
${optionsHtml}
|
||||
</div>
|
||||
|
||||
${
|
||||
this.prepResult
|
||||
? `
|
||||
<div class="feedback-panel ${this.prepResult.isCorrect ? "correct" : "incorrect"}">
|
||||
<div style="font-weight: 700; margin-bottom: 0.5rem;">${this.prepResult.message}</div>
|
||||
<div style="font-size: 1rem; font-weight: 600; margin-bottom: 0.45rem;">
|
||||
🗣️ „${this.tokenizeSentenceForHover(this.prepResult.fullSentence)}“
|
||||
<button class="icon-btn" style="width: 28px; height: 28px; margin-left: 0.35rem;" onclick="app.speak('${this.escapeJs(this.prepResult.fullSentence)}')">🔊</button>
|
||||
</div>
|
||||
<div style="font-size: 0.88rem; color: var(--text-secondary); margin-bottom: 0.5rem;">
|
||||
💡 <strong>Erklärung:</strong> ${d.explanation}
|
||||
</div>
|
||||
<div style="font-size: 0.86rem; background: rgba(255,255,255,0.75); padding: 0.55rem 0.8rem; border-radius: 8px; margin-bottom: 0.85rem;">
|
||||
🔗 <strong>Da-/Wo-Form:</strong> <code>${d.daForm}</code> / <code>${d.woForm}</code> — <em>„${this.tokenizeSentenceForHover(d.daExampleDe)}“</em>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="app.nextPrepDrill()">Nächster Satz →</button>
|
||||
</div>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
} else if (this.prepSubmode === "dawo") {
|
||||
const d = drills[this.daWoIndex % drills.length];
|
||||
const daChoices = ["darauf", "darüber", "darum", "daran", "davon", "dafür"];
|
||||
const clozeDaSentence = d.daExampleDe.replace(new RegExp(d.daForm, "i"), "___");
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="srs-card-stage">
|
||||
<div class="card-meta-top">
|
||||
<span class="category-tag">Da- & Wo-Baukasten • ${(this.daWoIndex % drills.length) + 1} / ${drills.length}</span>
|
||||
<span class="gender-badge gender-none">${d.verb} + ${d.preposition} (${d.grammaticalCase})</span>
|
||||
</div>
|
||||
|
||||
<div class="prompt-instruction">🔗 Wie ersetzen Deutsche einen ganzen Satzteil durch ein „da-Wort“?</div>
|
||||
<p style="font-size: 0.88rem; color: var(--text-secondary); margin-bottom: 1rem;">
|
||||
Regel: <code>da + Präposition</code> (bei Vokal a/o/u/ü mit <strong>-r-</strong> dazwischen: <em>da + r + auf = darauf</em>).
|
||||
</p>
|
||||
|
||||
<div class="cloze-box">
|
||||
<div class="cloze-de">${this.tokenizeSentenceForHover(clozeDaSentence)}</div>
|
||||
<div class="cloze-en" style="margin-top: 0.3rem;">Gesuchtes Verb: <strong>${d.verb} (${d.english})</strong></div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.65rem; margin: 1.25rem 0;">
|
||||
${daChoices
|
||||
.map(
|
||||
ch => `
|
||||
<button class="btn btn-secondary" style="justify-content: center; font-weight: 700;" onclick="app.checkDaWoAnswer('${ch}')">
|
||||
${ch}
|
||||
</button>
|
||||
`
|
||||
)
|
||||
.join("")}
|
||||
</div>
|
||||
|
||||
${
|
||||
this.daWoResult
|
||||
? `
|
||||
<div class="feedback-panel ${this.daWoResult.isCorrect ? "correct" : "incorrect"}">
|
||||
<div style="font-weight: 700; margin-bottom: 0.45rem;">${this.daWoResult.message}</div>
|
||||
<div style="font-size: 0.95rem; margin-bottom: 0.75rem;">
|
||||
„${this.tokenizeSentenceForHover(d.daExampleDe)}“
|
||||
<button class="icon-btn" style="width: 28px; height: 28px; margin-left: 0.35rem;" onclick="app.speak('${this.escapeJs(d.daExampleDe)}')">🔊</button>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="app.nextDaWoDrill()">Weiter →</button>
|
||||
</div>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
// Reference table
|
||||
container.innerHTML = `
|
||||
<div class="panel-card">
|
||||
<h2 style="font-size: 1.3rem; font-weight: 800; margin-bottom: 0.35rem;">📋 Die wichtigsten A2/B1 Verben mit fester Präposition</h2>
|
||||
<p style="font-size: 0.88rem; color: var(--text-secondary); margin-bottom: 1.25rem;">
|
||||
Lerne jedes Verb immer zusammen mit seiner Präposition und dem Fall (<span class="case-badge-akk">Akkusativ</span> oder <span class="case-badge-dat">Dativ</span>).
|
||||
</p>
|
||||
<div class="vocab-grid">
|
||||
${drills
|
||||
.map(
|
||||
d => `
|
||||
<div class="vocab-item-card">
|
||||
<div>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; gap: 0.5rem;">
|
||||
<strong style="font-size: 1.08rem;">${d.verb} <span style="color: var(--primary);">${d.preposition}</span></strong>
|
||||
<span class="${d.grammaticalCase === "Akkusativ" ? "case-badge-akk" : "case-badge-dat"}">+ ${d.grammaticalCase}</span>
|
||||
</div>
|
||||
<div style="font-weight: 600; margin: 0.35rem 0;">${d.english}</div>
|
||||
<div style="font-size: 0.8rem; color: var(--text-muted); margin-bottom: 0.45rem;">
|
||||
Da-/Wo-Form: <code>${d.daForm}</code> • <code>${d.woForm}?</code>
|
||||
</div>
|
||||
<div style="font-size: 0.86rem; background: var(--bg-main); padding: 0.55rem 0.7rem; border-radius: 8px;">
|
||||
„${this.tokenizeSentenceForHover(`${d.sentenceBefore} ${d.correctCombo} ${d.sentenceAfter}`)}“
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; padding-top: 0.5rem; border-top: 1px solid var(--border-color);">
|
||||
<button class="btn btn-secondary" style="padding: 0.3rem 0.65rem; font-size: 0.78rem;" onclick="app.speak('${this.escapeJs(`${d.sentenceBefore} ${d.correctCombo} ${d.sentenceAfter}`)}')">🔊 Anhören</button>
|
||||
<button class="btn btn-primary" style="padding: 0.3rem 0.65rem; font-size: 0.78rem;" onclick="app.saveLabItemToDeck('${this.escapeJs(d.verb + " " + d.preposition)}', null, '+ ${d.grammaticalCase} (${d.daForm})', '${this.escapeJs(d.english)}', '${this.escapeJs(`${d.sentenceBefore} ${d.correctCombo} ${d.sentenceAfter}`)}', '', '${this.escapeJs(d.explanation)}')">➕ Ins SRS-Deck</button>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
.join("")}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// === 3) HÖRTRAINING & AUSSPRACHE (LISTENING DICTATION & SHADOWING) ===
|
||||
computeWordDiffHtml(expectedSentence, userSentence) {
|
||||
const cleanWord = w => w.toLowerCase().replace(/[.,!?„“"']/g, "");
|
||||
const expectedWords = expectedSentence.split(/\s+/).filter(Boolean);
|
||||
const userWords = (userSentence || "").split(/\s+/).filter(Boolean).map(cleanWord);
|
||||
|
||||
let matchedCount = 0;
|
||||
const diffHtml = expectedWords
|
||||
.map(expW => {
|
||||
const normExp = cleanWord(expW);
|
||||
const foundIdx = userWords.indexOf(normExp);
|
||||
if (foundIdx !== -1) {
|
||||
userWords.splice(foundIdx, 1);
|
||||
matchedCount++;
|
||||
return `<span class="diff-word diff-ok word-token" data-word="${this.escapeJs(normExp)}">${expW}</span>`;
|
||||
}
|
||||
return `<span class="diff-word diff-miss word-token" data-word="${this.escapeJs(normExp)}">${expW}</span>`;
|
||||
})
|
||||
.join(" ");
|
||||
|
||||
const scorePct = Math.round((matchedCount / expectedWords.length) * 100);
|
||||
return { diffHtml, scorePct, matchedCount, total: expectedWords.length };
|
||||
}
|
||||
|
||||
checkListeningDictation(e) {
|
||||
e.preventDefault();
|
||||
const input = document.getElementById("listening-dictation-input");
|
||||
if (!input) return;
|
||||
const userText = input.value.trim();
|
||||
if (!userText) return;
|
||||
|
||||
const list = window.LISTENING_EXERCISES || [];
|
||||
const ex = list[this.listeningIndex % list.length];
|
||||
if (!ex) return;
|
||||
|
||||
const { diffHtml, scorePct, matchedCount, total } = this.computeWordDiffHtml(ex.de, userText);
|
||||
this.stats.totalChecks++;
|
||||
this.stats.sentencesWritten++;
|
||||
if (scorePct >= 80) this.stats.correctChecks++;
|
||||
|
||||
this.listeningResult = {
|
||||
userText,
|
||||
diffHtml,
|
||||
scorePct,
|
||||
matchedCount,
|
||||
total
|
||||
};
|
||||
this.saveState();
|
||||
this.renderListeningStage();
|
||||
}
|
||||
|
||||
toggleListeningHint() {
|
||||
this.listeningShowHint = !this.listeningShowHint;
|
||||
this.renderListeningStage();
|
||||
}
|
||||
|
||||
nextListeningExercise() {
|
||||
const list = window.LISTENING_EXERCISES || [];
|
||||
this.listeningIndex = (this.listeningIndex + 1) % list.length;
|
||||
this.listeningShowHint = false;
|
||||
this.listeningResult = null;
|
||||
this.speechResult = null;
|
||||
this.renderListeningStage();
|
||||
const nextEx = list[this.listeningIndex % list.length];
|
||||
if (nextEx) this.speak(nextEx.de, 0.96);
|
||||
}
|
||||
|
||||
startMicrophonePronunciationCheck() {
|
||||
const SpeechRec = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||
if (!SpeechRec) {
|
||||
this.showToast("Dein Browser unterstützt die Web Speech Mikrofon-Erkennung leider nicht (am besten Chrome oder Edge nutzen).");
|
||||
return;
|
||||
}
|
||||
|
||||
const list = window.LISTENING_EXERCISES || [];
|
||||
const ex = list[this.listeningIndex % list.length];
|
||||
if (!ex) return;
|
||||
|
||||
if (this.isRecordingMic && this.activeRecognition) {
|
||||
this.activeRecognition.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
const recognition = new SpeechRec();
|
||||
recognition.lang = "de-DE";
|
||||
recognition.interimResults = false;
|
||||
recognition.maxAlternatives = 1;
|
||||
|
||||
this.isRecordingMic = true;
|
||||
this.activeRecognition = recognition;
|
||||
this.renderListeningStage();
|
||||
|
||||
recognition.onresult = event => {
|
||||
const transcript = event.results?.[0]?.[0]?.transcript || "";
|
||||
const { diffHtml, scorePct } = this.computeWordDiffHtml(ex.de, transcript);
|
||||
this.speechResult = {
|
||||
transcript,
|
||||
diffHtml,
|
||||
scorePct
|
||||
};
|
||||
this.stats.sentencesWritten++;
|
||||
this.saveState();
|
||||
};
|
||||
|
||||
recognition.onerror = event => {
|
||||
this.showToast(`Mikrofon-Hinweis: ${event.error}`);
|
||||
};
|
||||
|
||||
recognition.onend = () => {
|
||||
this.isRecordingMic = false;
|
||||
this.renderListeningStage();
|
||||
};
|
||||
|
||||
recognition.start();
|
||||
}
|
||||
|
||||
renderListeningStage() {
|
||||
const container = document.getElementById("listening-stage-container");
|
||||
const list = window.LISTENING_EXERCISES || [];
|
||||
if (!container || list.length === 0) return;
|
||||
|
||||
const ex = list[this.listeningIndex % list.length];
|
||||
const wordInitialsHint = ex.de
|
||||
.split(" ")
|
||||
.map(w => w[0] + "___")
|
||||
.join(" ");
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="srs-card-stage">
|
||||
<div class="card-meta-top">
|
||||
<span class="category-tag">🎧 Hörtraining & Aussprache • ${(this.listeningIndex % list.length) + 1} / ${list.length} (${ex.level})</span>
|
||||
<span style="font-size: 0.82rem; font-weight: 600; color: var(--text-secondary);">${ex.situation}</span>
|
||||
</div>
|
||||
|
||||
<div class="audio-hero-box">
|
||||
<div>
|
||||
<div style="font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.85;">
|
||||
Schritt 1: Höre den deutschen Satz (ohne Text!)
|
||||
</div>
|
||||
<div style="font-size: 1.15rem; font-weight: 800; margin-top: 0.2rem;">
|
||||
Klicke auf Play und tippe unten genau, was du hörst.
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; gap: 0.6rem; flex-wrap: wrap;">
|
||||
<button class="btn btn-primary" onclick="app.speak('${this.escapeJs(ex.de)}', 0.96)">
|
||||
🔊 Normal abspielen (1.0x)
|
||||
</button>
|
||||
<button class="btn btn-light" onclick="app.speak('${this.escapeJs(ex.de)}', 0.78)">
|
||||
🐢 Langsam (0.8x)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem; flex-wrap: wrap; gap: 0.5rem;">
|
||||
<button type="button" class="btn btn-secondary" style="padding: 0.35rem 0.75rem; font-size: 0.8rem;" onclick="app.toggleListeningHint()">
|
||||
💡 ${this.listeningShowHint ? "Hinweis ausblenden" : "Englische Bedeutung & Anfangsbuchstaben zeigen"}
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" style="padding: 0.35rem 0.75rem; font-size: 0.8rem;" onclick="app.nextListeningExercise()">
|
||||
⏭️ Nächster Hör-Satz →
|
||||
</button>
|
||||
</div>
|
||||
|
||||
${
|
||||
this.listeningShowHint
|
||||
? `
|
||||
<div class="cloze-box" style="margin-bottom: 1rem;">
|
||||
<div style="font-weight: 700; font-size: 0.92rem;">🇬🇧 Bedeutung: „${ex.en}“</div>
|
||||
<div style="font-size: 0.84rem; color: var(--text-secondary); margin-top: 0.25rem;">
|
||||
🔤 Wort-Struktur (${ex.de.split(" ").length} Wörter): <code>${wordInitialsHint}</code>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
|
||||
<form onsubmit="app.checkListeningDictation(event)">
|
||||
<div class="prompt-instruction">✍️ Diktat: Tippe den gehörten deutschen Satz</div>
|
||||
<div class="answer-input-row">
|
||||
<input
|
||||
type="text"
|
||||
id="listening-dictation-input"
|
||||
class="german-input"
|
||||
placeholder="Tippe hier, was du gehört hast..."
|
||||
value="${this.listeningResult ? this.escapeJs(this.listeningResult.userText) : ""}"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<button type="submit" class="btn btn-primary">Diktat prüfen</button>
|
||||
</div>
|
||||
${this.renderUmlautBar("listening-dictation-input")}
|
||||
</form>
|
||||
|
||||
${
|
||||
this.listeningResult
|
||||
? `
|
||||
<div class="feedback-panel ${this.listeningResult.scorePct >= 80 ? "correct" : "incorrect"}">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem; flex-wrap: wrap;">
|
||||
<strong>🎯 Diktat-Ergebnis: ${this.listeningResult.scorePct}% (${this.listeningResult.matchedCount}/${this.listeningResult.total} Wörter erkannt)</strong>
|
||||
<button class="btn btn-secondary" style="padding: 0.3rem 0.7rem; font-size: 0.8rem;" onclick="app.speak('${this.escapeJs(ex.de)}', 0.96)">🔊 Nochmal hören</button>
|
||||
</div>
|
||||
<div style="margin-bottom: 0.5rem; line-height: 1.9;">
|
||||
${this.listeningResult.diffHtml}
|
||||
</div>
|
||||
<div style="font-size: 0.86rem; color: var(--text-secondary); margin-bottom: 0.45rem;">
|
||||
🇬🇧 „${ex.en}“
|
||||
</div>
|
||||
<div style="font-size: 0.84rem; color: var(--text-muted);">
|
||||
💡 <strong>Hör-Fokus:</strong> ${ex.focusNote}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
|
||||
<div style="margin-top: 1.5rem; padding-top: 1.25rem; border-top: 1px solid var(--border-color);">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.75rem;">
|
||||
<div>
|
||||
<div class="prompt-instruction">🎙️ Schritt 2: Shadowing & Aussprache-Check (Optional)</div>
|
||||
<div style="font-size: 0.88rem; color: var(--text-secondary);">
|
||||
Sprich den Satz laut auf Deutsch nach — die Spracherkennung prüft deine Aussprache Wort für Wort!
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary ${this.isRecordingMic ? "mic-btn-recording" : ""}"
|
||||
onclick="app.startMicrophonePronunciationCheck()"
|
||||
>
|
||||
${this.isRecordingMic ? "🔴 Höre zu... (Jetzt sprechen)" : "🎙️ Satz laut sprechen"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
${
|
||||
this.speechResult
|
||||
? `
|
||||
<div class="feedback-panel ${this.speechResult.scorePct >= 75 ? "correct" : ""}" style="margin-top: 0.85rem;">
|
||||
<div style="font-weight: 700; margin-bottom: 0.35rem;">
|
||||
🗣️ Erkannt: „${this.speechResult.transcript}“ (${this.speechResult.scorePct}% Übereinstimmung)
|
||||
</div>
|
||||
<div>${this.speechResult.diffHtml}</div>
|
||||
</div>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
renderAll() {
|
||||
this.updateDashboardCounters();
|
||||
this.renderDailySpotlight();
|
||||
|
||||
+34
-1
@@ -236,5 +236,38 @@ window.HOVER_DICT = {
|
||||
"exportieren": "to export", "lernstand": "learning progress", "zurücksetzen": "to reset",
|
||||
"verständnis": "understanding / comprehension (das Verständnis)",
|
||||
"klicke": "click (imperative)", "wähle": "choose / select (imperative)", "versuche": "try (imperative)",
|
||||
"fahre": "move / hover / drive", "maus": "mouse (die Maus)", "beliebiges": "any", "stammform": "base form / lemma"
|
||||
"fahre": "move / hover / drive", "maus": "mouse (die Maus)", "beliebiges": "any", "stammform": "base form / lemma",
|
||||
|
||||
// Labs: Wortbaukasten, Hörtraining & Verben mit Präposition
|
||||
"wortbaukasten": "Word Building Kit / Compound & Prefix Lab (der Wortbaukasten)",
|
||||
"hörtraining": "Listening Training (das Hörtraining)",
|
||||
"diktat": "Dictation (das Diktat)",
|
||||
"rektion": "Verb Preposition & Case Governance (die Rektion)",
|
||||
"annehmen": "to accept / assume", "angenommen": "accepted / assumed", "paket": "package / parcel (das Paket)", "jobangebot": "job offer (das Jobangebot)",
|
||||
"abnehmen": "to lose weight / take off", "abgenommen": "lost weight / taken off", "kilo": "kilogram(s) (das Kilo)", "frühling": "spring (der Frühling)",
|
||||
"nimmst": "take (nimmst ... teil = take part)", "teil": "part (der/das Teil)", "kollegen": "colleagues (die Kollegen)", "besprechung": "meeting (die Besprechung)", "cappuccino": "cappuccino (der Cappuccino)",
|
||||
"unternehmen": "to undertake / do an activity", "unternommen": "undertaken / done", "langen": "long", "schönes": "something nice", "tolle": "great / awesome", "ausflüge": "day trips / excursions (die Ausflüge)",
|
||||
"übernehmen": "to take over / cover (costs)", "übernommen": "taken over / covered", "übernehme": "take over / cover", "kompletten": "complete / entire",
|
||||
"vorstellen": "to introduce / imagine", "neuer": "new", "nachbar": "neighbor (der Nachbar)", "zweiten": "second", "ganzes": "whole / entire", "leben": "to live",
|
||||
"bestellen": "to order", "bestellt": "ordered", "gewählt": "chosen / selected", "winterstiefel": "winter boots (die Winterstiefel)", "internet": "internet (das Internet)",
|
||||
"ausstellen": "to issue (a document/prescription)", "ausgestellt": "issued",
|
||||
"feststellen": "to realize / notice / determine", "festgestellt": "realized / noticed", "portemonnaie": "wallet (das Portemonnaie)", "liegt": "lies / is located", "auspacken": "unpacking (das Auspacken)", "fehlt": "is missing",
|
||||
"welches": "which", "vorgeschlagen": "suggested / proposed", "nachschlagen": "to look up (in a dictionary)", "unbekannte": "unknown", "mühsam": "tediously / painstakingly",
|
||||
"ankommen": "to arrive", "baustelle": "construction site / roadworks (die Baustelle)", "bekommen": "to get / receive", "steuer": "tax (die Steuer)", "nachricht": "message (die Nachricht)",
|
||||
"lust": "desire / feel like (Lust haben = to feel like)", "glas": "glass (das Glas)", "wein": "wine (der Wein)",
|
||||
"krankenversicherungskarte": "health insurance card (die Krankenversicherungskarte)", "legen": "to lay / present (legen ... vor)", "empfang": "reception desk (der Empfang)",
|
||||
"wohnungsbesichtigung": "apartment viewing (die Wohnungsbesichtigung)", "samstagmittag": "Saturday noon", "online": "online", "freien": "free / available",
|
||||
"bewahr": "keep / save (bewahr ... auf)", "falls": "in case / if", "gerät": "device / appliance (das Gerät)",
|
||||
"feierabendbier": "after-work beer (das Feierabendbier)", "biergarten": "beer garden (der Biergarten)", "kühles": "cool / cold",
|
||||
"nebenkostenabrechnung": "annual utility bill statement (die Nebenkostenabrechnung)", "nichts": "nothing", "nachzahlen": "to pay extra / back-pay",
|
||||
"anschlussverbindung": "connecting train / transit connection (die Anschlussverbindung)", "warten": "to wait", "warte": "wait", "fahrgäste": "passengers (die Fahrgäste)",
|
||||
"anrufen": "to call (on the phone)", "supermarktkasse": "supermarket checkout (die Supermarktkasse)", "wochenendausflug": "weekend trip (der Wochenendausflug)", "planen": "to plan", "reklamation": "complaint / return (die Reklamation)", "kaufhaus": "department store (das Kaufhaus)",
|
||||
"kümmern": "to take care of (sich kümmern um)", "abendessen": "dinner / supper (das Abendessen)", "darum": "about it / of it (da + um)", "worum": "about what (wo + um)",
|
||||
"freuen": "to be glad / look forward to", "zukunft": "future (die Zukunft)", "meer": "sea / ocean (das Meer)", "darauf": "to it / for it (da + auf)",
|
||||
"gegenwart": "present (die Gegenwart)", "vergangenheit": "past (die Vergangenheit)", "nette": "nice / kind", "gefreut": "been happy / looked forward", "geholfen": "helped", "darüber": "about it (da + über)", "worüber": "about what (wo + über)",
|
||||
"workshop": "workshop (der Workshop)", "daran": "in it / of it / to it (da + an)", "woran": "in what / of what (wo + an)",
|
||||
"abhängen": "to depend (on)", "davon": "on it / from it (da + von)", "wovon": "on what / from what (wo + von)",
|
||||
"gewöhnen": "to get used to (sich gewöhnen an)", "gewöhnt": "accustomed / used to", "ruhigen": "quiet / peaceful", "sonntage": "Sundays (die Sonntage)", "anfang": "beginning (am Anfang = at the beginning)", "bürokratie": "bureaucracy (die Bürokratie)",
|
||||
"umzug": "move / relocation (der Umzug)", "interessieren": "to be interested (in)", "interessiert": "interested", "interessierst": "are interested", "wofür": "in what / for what (wo + für)", "dafür": "in it / for it (da + für)", "besonders": "especially / particularly", "kultur": "culture (die Kultur)",
|
||||
"erinnern": "to remember (sich erinnern an)", "erinnerst": "remember", "erinnere": "remember"
|
||||
};
|
||||
+562
@@ -0,0 +1,562 @@
|
||||
// WortKraft Labs Data — 1) Wortbaukasten (Compounds & Prefixes), 2) Hörtraining (Dictation & Shadowing), 3) Verben mit Präposition (Rektion & Da-/Wo-)
|
||||
|
||||
window.WORD_BUILDER_DATA = {
|
||||
verbRoots: [
|
||||
{
|
||||
root: "nehmen",
|
||||
baseMeaning: "to take (nimmt • nahm • hat genommen)",
|
||||
derivatives: [
|
||||
{
|
||||
word: "annehmen",
|
||||
prefix: "an-",
|
||||
separable: true,
|
||||
forms: "nimmt an • nahm an • hat angenommen",
|
||||
english: "to accept (a package/offer) / to assume",
|
||||
logic: "an (at/toward) + nehmen (take) = take toward oneself → accept",
|
||||
exampleDe: "Kannst du heute Nachmittag bitte das Paket für mich annehmen?",
|
||||
exampleEn: "Can you please accept the package for me this afternoon?",
|
||||
quizCloze: "Er hat das Jobangebot in München sofort ___.",
|
||||
quizAnswer: "angenommen"
|
||||
},
|
||||
{
|
||||
word: "abnehmen",
|
||||
prefix: "ab-",
|
||||
separable: true,
|
||||
forms: "nimmt ab • nahm ab • hat abgenommen",
|
||||
english: "to take off / to lose weight / to decrease",
|
||||
logic: "ab (off/down) + nehmen (take) = take off weight or load",
|
||||
exampleDe: "Seit ich jeden Tag mit dem Fahrrad fahre, habe ich drei Kilo abgenommen.",
|
||||
exampleEn: "Since I started riding my bike every day, I've lost three kilos.",
|
||||
quizCloze: "Im Frühling möchte ich ein paar Kilo ___.",
|
||||
quizAnswer: "abnehmen"
|
||||
},
|
||||
{
|
||||
word: "teilnehmen",
|
||||
prefix: "teil-",
|
||||
separable: true,
|
||||
forms: "nimmt teil • nahm teil • hat teilgenommen",
|
||||
english: "to participate / take part (an + Dative)",
|
||||
logic: "Teil (part) + nehmen (take) = literally 'to take part'",
|
||||
exampleDe: "Nimmst du morgen Abend auch an unserem Sprach-Stammtisch teil?",
|
||||
exampleEn: "Are you also taking part in our language meetup tomorrow evening?",
|
||||
quizCloze: "Wie viele Kollegen ___ heute an der Besprechung ___?",
|
||||
quizAnswer: "nehmen teil"
|
||||
},
|
||||
{
|
||||
word: "mitnehmen",
|
||||
prefix: "mit-",
|
||||
separable: true,
|
||||
forms: "nimmt mit • nahm mit • hat mitgenommen",
|
||||
english: "to take along / to get food to-go ('zum Mitnehmen')",
|
||||
logic: "mit (with) + nehmen (take) = take along with you",
|
||||
exampleDe: "Einen großen Cappuccino zum Mitnehmen, bitte!",
|
||||
exampleEn: "A large cappuccino to go, please!",
|
||||
quizCloze: "Möchten Sie den Kaffee hier trinken oder zum ___?",
|
||||
quizAnswer: "Mitnehmen"
|
||||
},
|
||||
{
|
||||
word: "unternehmen",
|
||||
prefix: "unter-",
|
||||
separable: false,
|
||||
forms: "unternimmt • unternahm • hat unternommen (NO -ge-!)",
|
||||
english: "to undertake / to do an activity together",
|
||||
logic: "Inseparable prefix unter- (stressed on -nehmen): to venture into doing something",
|
||||
exampleDe: "Wollen wir am langen Wochenende etwas Schönes zusammen unternehmen?",
|
||||
exampleEn: "Shall we do something nice together over the long weekend?",
|
||||
quizCloze: "Wir haben im Urlaub jeden Tag tolle Ausflüge ___.",
|
||||
quizAnswer: "unternommen"
|
||||
},
|
||||
{
|
||||
word: "übernehmen",
|
||||
prefix: "über-",
|
||||
separable: false,
|
||||
forms: "übernimmt • übernahm • hat übernommen (NO -ge-!)",
|
||||
english: "to take over (a task, shift, or cost)",
|
||||
logic: "Inseparable prefix über-: take over responsibility or payment",
|
||||
exampleDe: "Die Krankenkasse hat die kompletten Kosten für die Untersuchung übernommen.",
|
||||
exampleEn: "The health insurance provider covered the entire cost of the examination.",
|
||||
quizCloze: "Keine Sorge, heute Abend ___ ich die Rechnung!",
|
||||
quizAnswer: "übernehme"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
root: "stellen",
|
||||
baseMeaning: "to place / put upright / set (stellt • stellte • hat gestellt)",
|
||||
derivatives: [
|
||||
{
|
||||
word: "vorstellen",
|
||||
prefix: "vor-",
|
||||
separable: true,
|
||||
forms: "stellt vor • stellte vor • hat vorgestellt",
|
||||
english: "to introduce (sich + Acc) / to imagine (sich + Dat)",
|
||||
logic: "vor (in front of) + stellen (place) = place before someone's eyes",
|
||||
exampleDe: "Darf ich mich kurz vorstellen? Ich bin dein neuer Nachbar aus dem zweiten Stock.",
|
||||
exampleEn: "May I introduce myself briefly? I'm your new neighbor from the second floor.",
|
||||
quizCloze: "Kannst du dir ___, ein ganzes Jahr in Berlin zu leben?",
|
||||
quizAnswer: "vorstellen"
|
||||
},
|
||||
{
|
||||
word: "bestellen",
|
||||
prefix: "be-",
|
||||
separable: false,
|
||||
forms: "bestellt • bestellte • hat bestellt (NO -ge-!)",
|
||||
english: "to order (food in a restaurant or items online)",
|
||||
logic: "Inseparable be- + stellen: arrange for delivery/service",
|
||||
exampleDe: "Haben Sie schon gewählt, oder möchten Sie erst Getränke bestellen?",
|
||||
exampleEn: "Have you already chosen, or would you like to order drinks first?",
|
||||
quizCloze: "Ich habe die neuen Winterstiefel gestern im Internet ___.",
|
||||
quizAnswer: "bestellt"
|
||||
},
|
||||
{
|
||||
word: "ausstellen",
|
||||
prefix: "aus-",
|
||||
separable: true,
|
||||
forms: "stellt aus • stellte aus • hat ausgestellt",
|
||||
english: "to issue (a document/certificate/prescription) / to exhibit",
|
||||
logic: "aus (out) + stellen = put out officially",
|
||||
exampleDe: "Die Ärztin hat mir sofort ein Rezept und eine Krankschreibung ausgestellt.",
|
||||
exampleEn: "The doctor immediately issued me a prescription and a sick note.",
|
||||
quizCloze: "Das Bürgeramt hat meinen neuen Reisepass in drei Wochen ___.",
|
||||
quizAnswer: "ausgestellt"
|
||||
},
|
||||
{
|
||||
word: "feststellen",
|
||||
prefix: "fest-",
|
||||
separable: true,
|
||||
forms: "stellt fest • stellte fest • hat festgestellt",
|
||||
english: "to realize / notice / determine",
|
||||
logic: "fest (firm/fixed) + stellen = establish as a fact",
|
||||
exampleDe: "An der Kasse habe ich plötzlich festgestellt, dass mein Portemonnaie zu Hause liegt.",
|
||||
exampleEn: "At the checkout I suddenly realized that my wallet is at home.",
|
||||
quizCloze: "Beim Auspacken musste ich leider ___, dass ein Teil fehlt.",
|
||||
quizAnswer: "feststellen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
root: "schlagen",
|
||||
baseMeaning: "to hit / beat / strike (schlägt • schlug • hat geschlagen)",
|
||||
derivatives: [
|
||||
{
|
||||
word: "vorschlagen",
|
||||
prefix: "vor-",
|
||||
separable: true,
|
||||
forms: "schlägt vor • schlug vor • hat vorgeschlagen",
|
||||
english: "to suggest / propose",
|
||||
logic: "vor (forward) + schlagen = put a proposal forward",
|
||||
exampleDe: "Ich schlage vor, dass wir uns direkt vor dem Rathaus treffen.",
|
||||
exampleEn: "I suggest that we meet right in front of the town hall.",
|
||||
quizCloze: "Welches Restaurant hast du für heute Abend ___?",
|
||||
quizAnswer: "vorgeschlagen"
|
||||
},
|
||||
{
|
||||
word: "nachschlagen",
|
||||
prefix: "nach-",
|
||||
separable: true,
|
||||
forms: "schlägt nach • schlug nach • hat nachgeschlagen",
|
||||
english: "to look up (a word in a dictionary)",
|
||||
logic: "nach (after) + schlagen = flip through pages after something",
|
||||
exampleDe: "Mit dem Hover-Übersetzer musst du unbekannte Wörter nicht mehr mühsam nachschlagen.",
|
||||
exampleEn: "With the hover translator, you no longer have to tediously look up unknown words.",
|
||||
quizCloze: "Wenn ich den Artikel nicht weiß, ___ ich ihn kurz im Wörterbuch ___.",
|
||||
quizAnswer: "schlage nach"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
root: "kommen",
|
||||
baseMeaning: "to come (kommt • kam • ist gekommen)",
|
||||
derivatives: [
|
||||
{
|
||||
word: "ankommen",
|
||||
prefix: "an-",
|
||||
separable: true,
|
||||
forms: "kommt an • kam an • ist angekommen",
|
||||
english: "to arrive (also: 'Es kommt darauf an' = it depends!)",
|
||||
logic: "an (at) + kommen = arrive at destination",
|
||||
exampleDe: "Unser ICE ist trotz der Baustelle pünktlich in Hamburg angekommen.",
|
||||
exampleEn: "Our ICE train arrived on time in Hamburg despite the roadworks.",
|
||||
quizCloze: "Um wie viel Uhr ___ dein Zug heute Abend am Hauptbahnhof ___?",
|
||||
quizAnswer: "kommt an"
|
||||
},
|
||||
{
|
||||
word: "bekommen",
|
||||
prefix: "be-",
|
||||
separable: false,
|
||||
forms: "bekommt • bekam • hat bekommen (FALSE FRIEND: means 'to get/receive', NOT 'become'!)",
|
||||
english: "to get / to receive",
|
||||
logic: "Inseparable be- + kommen: something comes to you",
|
||||
exampleDe: "Ohne Anmeldung beim Bürgeramt bekommt man leider keine Steuer-ID.",
|
||||
exampleEn: "Without registering at the civic office, you unfortunately don't get a tax ID.",
|
||||
quizCloze: "Hast du meine Nachricht auf WhatsApp schon ___?",
|
||||
quizAnswer: "bekommen"
|
||||
},
|
||||
{
|
||||
word: "vorbeikommen",
|
||||
prefix: "vorbei-",
|
||||
separable: true,
|
||||
forms: "kommt vorbei • kam vorbei • ist vorbeigekommen",
|
||||
english: "to drop by / come over / swing by",
|
||||
logic: "vorbei (by/past) + kommen = come by someone's place",
|
||||
exampleDe: "Wenn du Lust hast, kannst du heute Abend gerne auf ein Glas Wein vorbeikommen.",
|
||||
exampleEn: "If you feel like it, you're welcome to drop by for a glass of wine tonight.",
|
||||
quizCloze: "Sie können heute um elf Uhr direkt in der Praxis ___.",
|
||||
quizAnswer: "vorbeikommen"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
compounds: [
|
||||
{
|
||||
id: "comp1",
|
||||
compound: "Krankenversicherungskarte",
|
||||
finalArticle: "die",
|
||||
plural: "die Krankenversicherungskarten",
|
||||
english: "health insurance card",
|
||||
parts: [
|
||||
{ word: "Kranken-", article: null, meaning: "sick people's (from der/die Kranke)" },
|
||||
{ word: "Versicherungs-", article: "die", meaning: "insurance (die Versicherung)" },
|
||||
{ word: "Karte", article: "die", meaning: "card (die Karte → determines 'die'!)" }
|
||||
],
|
||||
ruleNote: "Even though this word has 25 letters, the last building block is 'die Karte' (feminine), so the entire compound is 'die Krankenversicherungskarte'!",
|
||||
exampleDe: "Bitte legen Sie am Empfang zuerst Ihre Krankenversicherungskarte vor."
|
||||
},
|
||||
{
|
||||
id: "comp2",
|
||||
compound: "Wohnungsbesichtigung",
|
||||
finalArticle: "die",
|
||||
plural: "die Wohnungsbesichtigungen",
|
||||
english: "apartment viewing",
|
||||
parts: [
|
||||
{ word: "Wohnungs-", article: "die", meaning: "apartment (die Wohnung + linking -s-)" },
|
||||
{ word: "Besichtigung", article: "die", meaning: "viewing / inspection (die Besichtigung → determines 'die'!)" }
|
||||
],
|
||||
ruleNote: "Notice the 'Fugen-s' (linking -s-) after -ung nouns when forming compounds: Wohnung + s + Besichtigung.",
|
||||
exampleDe: "Am Samstagmittag gehen wir zusammen zu einer Wohnungsbesichtigung."
|
||||
},
|
||||
{
|
||||
id: "comp3",
|
||||
compound: "Bürgeramt",
|
||||
finalArticle: "das",
|
||||
plural: "die Bürgerämter",
|
||||
english: "citizens' registration office",
|
||||
parts: [
|
||||
{ word: "Bürger", article: "der", meaning: "citizen (der Bürger)" },
|
||||
{ word: "Amt", article: "das", meaning: "office / department (das Amt → determines 'das'!)" }
|
||||
],
|
||||
ruleNote: "'der Bürger' is masculine, but the final noun is 'das Amt' (neuter)—so it is ALWAYS 'das Bürgeramt'!",
|
||||
exampleDe: "Ich habe online endlich einen freien Termin beim Bürgeramt gefunden."
|
||||
},
|
||||
{
|
||||
id: "comp4",
|
||||
compound: "Kassenbon",
|
||||
finalArticle: "der",
|
||||
plural: "die Kassenbons",
|
||||
english: "checkout receipt",
|
||||
parts: [
|
||||
{ word: "Kassen-", article: "die", meaning: "cash register (die Kasse)" },
|
||||
{ word: "Bon", article: "der", meaning: "voucher / slip (der Bon → determines 'der'!)" }
|
||||
],
|
||||
ruleNote: "'die Kasse' is feminine, but 'der Bon' is masculine—so the receipt is 'der Kassenbon'!",
|
||||
exampleDe: "Bewahr den Kassenbon gut auf, falls wir das Gerät umtauschen müssen."
|
||||
},
|
||||
{
|
||||
id: "comp5",
|
||||
compound: "Feierabendbier",
|
||||
finalArticle: "das",
|
||||
plural: "die Feierabendbiere",
|
||||
english: "after-work beer",
|
||||
parts: [
|
||||
{ word: "Feier", article: "die", meaning: "celebration / rest (die Feier)" },
|
||||
{ word: "Abend", article: "der", meaning: "evening (der Abend)" },
|
||||
{ word: "Bier", article: "das", meaning: "beer (das Bier → determines 'das'!)" }
|
||||
],
|
||||
ruleNote: "Three nouns with three different genders (die Feier + der Abend + das Bier) → last word wins: 'das Feierabendbier'!",
|
||||
exampleDe: "Nach der langen Arbeitswoche trinken wir im Biergarten ein kühles Feierabendbier."
|
||||
},
|
||||
{
|
||||
id: "comp6",
|
||||
compound: "Mehrwegbecher",
|
||||
finalArticle: "der",
|
||||
plural: "die Mehrwegbecher",
|
||||
english: "reusable cup (with deposit)",
|
||||
parts: [
|
||||
{ word: "Mehrweg-", article: "der", meaning: "multi-way / reusable (mehr + der Weg)" },
|
||||
{ word: "Becher", article: "der", meaning: "cup / mug (der Becher → determines 'der'!)" }
|
||||
],
|
||||
ruleNote: "German contrasts 'Mehrweg' (reusable, literally 'more-way') with 'Einweg' (single-use/disposable, 'one-way').",
|
||||
exampleDe: "Für den Mehrwegbecher auf dem Weihnachtsmarkt zahlt man zwei Euro Pfand."
|
||||
},
|
||||
{
|
||||
id: "comp7",
|
||||
compound: "Nebenkostenabrechnung",
|
||||
finalArticle: "die",
|
||||
plural: "die Nebenkostenabrechnungen",
|
||||
english: "annual utility bill statement",
|
||||
parts: [
|
||||
{ word: "Neben-", article: null, meaning: "additional / side" },
|
||||
{ word: "Kosten-", article: "die (Pl.)", meaning: "costs (die Kosten)" },
|
||||
{ word: "Abrechnung", article: "die", meaning: "settlement / statement (die Abrechnung → determines 'die'!)" }
|
||||
],
|
||||
ruleNote: "Every tenant in Germany receives 'die Nebenkostenabrechnung' once a year comparing advance utility payments with actual heating/water usage.",
|
||||
exampleDe: "Zum Glück mussten wir bei der Nebenkostenabrechnung dieses Jahr nichts nachzahlen."
|
||||
},
|
||||
{
|
||||
id: "comp8",
|
||||
compound: "Anschlussverbindung",
|
||||
finalArticle: "die",
|
||||
plural: "die Anschlussverbindungen",
|
||||
english: "connecting train / transit connection",
|
||||
parts: [
|
||||
{ word: "Anschluss-", article: "der", meaning: "connection / follow-up (der Anschluss)" },
|
||||
{ word: "Verbindung", article: "die", meaning: "link / connection (die Verbindung → determines 'die'!)" }
|
||||
],
|
||||
ruleNote: "'der Anschluss' is masculine, 'die Verbindung' is feminine → 'die Anschlussverbindung'.",
|
||||
exampleDe: "Wegen der Verspätung warten viele Fahrgäste auf ihre Anschlussverbindung."
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// 2) LISTENING DICTATION & SHADOWING SENTENCES (HÖRTRAINING)
|
||||
window.LISTENING_EXERCISES = [
|
||||
{
|
||||
id: "listen1",
|
||||
level: "A2",
|
||||
situation: "Beim Arzt anrufen (Calling the doctor's office)",
|
||||
de: "Ich möchte heute bitte kurzfristig einen Termin vereinbaren.",
|
||||
en: "I would like to schedule an appointment on short notice today, please.",
|
||||
focusNote: "Listen for 'kurzfristig' (at short notice) and the unstressed 'einen Termin'."
|
||||
},
|
||||
{
|
||||
id: "listen2",
|
||||
level: "A2",
|
||||
situation: "Termin verschieben (Postponing an appointment)",
|
||||
de: "Leider muss ich unsere Besichtigung auf nächste Woche verschieben.",
|
||||
en: "Unfortunately I have to postpone our viewing to next week.",
|
||||
focusNote: "Notice the V2 inversion after 'Leider': 'Leider muss ich...' and the -ung ending on 'Besichtigung'."
|
||||
},
|
||||
{
|
||||
id: "listen3",
|
||||
level: "A2",
|
||||
situation: "An der Supermarktkasse (At the supermarket checkout)",
|
||||
de: "Brauchen Sie den Kassenbon, oder haben Sie noch einen Pfandbon?",
|
||||
en: "Do you need the receipt, or do you still have a bottle deposit slip?",
|
||||
focusNote: "Two classic German checkout compounds: 'der Kassenbon' and 'der Pfandbon'."
|
||||
},
|
||||
{
|
||||
id: "listen4",
|
||||
level: "A2 / B1",
|
||||
situation: "Feierabend mit Kollegen (After work with colleagues)",
|
||||
de: "Ich schlage vor, dass wir endlich Feierabend machen.",
|
||||
en: "I suggest that we finally call it a day.",
|
||||
focusNote: "Hear how the separable verb 'vorschlagen' splits in the main clause ('Ich schlage vor'), while 'machen' goes to the end after 'dass'."
|
||||
},
|
||||
{
|
||||
id: "listen5",
|
||||
level: "A2 / B1",
|
||||
situation: "Auf dem Bahnsteig (On the train platform)",
|
||||
de: "Der Zug hat heute leider zwanzig Minuten Verspätung, deshalb müssen wir in Hannover umsteigen.",
|
||||
en: "The train unfortunately has a twenty-minute delay today, therefore we have to transfer in Hanover.",
|
||||
focusNote: "Notice the connector 'deshalb' followed immediately by the verb 'müssen'."
|
||||
},
|
||||
{
|
||||
id: "listen6",
|
||||
level: "B1",
|
||||
situation: "Wochenendausflug planen (Planning a weekend trip)",
|
||||
de: "Keine Sorge, ich kümmere mich um die Unterkunft, obwohl es am Wochenende regnen soll.",
|
||||
en: "No worries, I'll take care of the accommodation, even though it's supposed to rain on the weekend.",
|
||||
focusNote: "Listen for 'ich kümmere mich um' (+ Accusative) and 'obwohl' kicking 'regnen soll' to the very end."
|
||||
},
|
||||
{
|
||||
id: "listen7",
|
||||
level: "A2",
|
||||
situation: "Kurze Nachricht an Freunde (Quick message to friends)",
|
||||
de: "Gibst du mir bitte kurz Bescheid, sobald du zu Hause angekommen bist?",
|
||||
en: "Will you please let me know briefly as soon as you have arrived home?",
|
||||
focusNote: "Natural everyday idiom 'Bescheid geben' + subordinate clause connector 'sobald'."
|
||||
},
|
||||
{
|
||||
id: "listen8",
|
||||
level: "B1",
|
||||
situation: "Reklamation im Kaufhaus (Exchanging an item at a store)",
|
||||
de: "Ich möchte diese Jacke gerne umtauschen, weil der Reißverschluss kaputt ist.",
|
||||
en: "I would like to exchange this jacket because the zipper is broken.",
|
||||
focusNote: "Notice 'diese Jacke umtauschen' and 'der Reißverschluss' (zipper)."
|
||||
}
|
||||
];
|
||||
|
||||
// 3) VERBS WITH PREPOSITIONS & CASES (REKTION DER VERBEN + DA-/WO- TRAINER)
|
||||
window.PREPOSITION_DRILLS = [
|
||||
{
|
||||
id: "prep1",
|
||||
verb: "sich kümmern",
|
||||
preposition: "um",
|
||||
grammaticalCase: "Akkusativ",
|
||||
daForm: "darum",
|
||||
woForm: "worum",
|
||||
english: "to take care of / look after",
|
||||
sentenceBefore: "Keine Sorge, ich kümmere mich heute",
|
||||
sentenceAfter: "Abendessen.",
|
||||
nounHint: "das Abendessen (neut.)",
|
||||
correctCombo: "um das",
|
||||
acceptedAnswers: ["um das", "ums"],
|
||||
options: ["um das", "für das", "über das", "mit dem"],
|
||||
daExampleDe: "Wir brauchen noch Getränke. — Ich kümmere mich darum!",
|
||||
explanation: "'sich kümmern' ALWAYS takes 'um + Akkusativ' (um das Abendessen / ums Abendessen). For things: 'darum' / 'Worum kümmerst du dich?'"
|
||||
},
|
||||
{
|
||||
id: "prep2",
|
||||
verb: "sich freuen (Zukunft)",
|
||||
preposition: "auf",
|
||||
grammaticalCase: "Akkusativ",
|
||||
daForm: "darauf",
|
||||
woForm: "worauf",
|
||||
english: "to look forward to (something in the future)",
|
||||
sentenceBefore: "Nach der langen Arbeitswoche freue ich mich riesig",
|
||||
sentenceAfter: "Feierabend!",
|
||||
nounHint: "der Feierabend (mask.)",
|
||||
correctCombo: "auf den",
|
||||
acceptedAnswers: ["auf den"],
|
||||
options: ["auf den", "auf dem", "über den", "für den"],
|
||||
daExampleDe: "Am Freitag fahren wir ans Meer — ich freue mich schon sehr darauf!",
|
||||
explanation: "Future events take 'sich freuen AUF + Akkusativ'. Since 'der Feierabend' is masculine, Accusative changes 'der' to 'den' → 'auf den Feierabend'."
|
||||
},
|
||||
{
|
||||
id: "prep3",
|
||||
verb: "sich freuen (Gegenwart/Vergangenheit)",
|
||||
preposition: "über",
|
||||
grammaticalCase: "Akkusativ",
|
||||
daForm: "darüber",
|
||||
woForm: "worüber",
|
||||
english: "to be happy about (something received or already happening)",
|
||||
sentenceBefore: "Sie hat sich gestern sehr",
|
||||
sentenceAfter: "nette Einladung gefreut.",
|
||||
nounHint: "die Einladung (fem.)",
|
||||
correctCombo: "über die",
|
||||
acceptedAnswers: ["über die", "ueber die"],
|
||||
options: ["über die", "auf die", "über der", "von der"],
|
||||
daExampleDe: "Du hast mir sehr geholfen — ich habe mich wirklich darüber gefreut.",
|
||||
explanation: "When something has already happened or you received a gift/invitation, use 'sich freuen ÜBER + Akkusativ' ('über die Einladung')."
|
||||
},
|
||||
{
|
||||
id: "prep4",
|
||||
verb: "teilnehmen",
|
||||
preposition: "an",
|
||||
grammaticalCase: "Dativ",
|
||||
daForm: "daran",
|
||||
woForm: "woran",
|
||||
english: "to participate in / attend",
|
||||
sentenceBefore: "Möchtest du nächste Woche auch",
|
||||
sentenceAfter: "Deutschkurs teilnehmen?",
|
||||
nounHint: "der Deutschkurs (mask.)",
|
||||
correctCombo: "an dem",
|
||||
acceptedAnswers: ["an dem", "am"],
|
||||
options: ["an dem (am)", "in dem", "auf den", "zu dem"],
|
||||
daExampleDe: "Morgen findet ein Workshop statt. Nimmst du auch daran teil?",
|
||||
explanation: "'teilnehmen' takes 'an + Dativ'! Masculine 'der Deutschkurs' becomes 'dem' in Dative → 'an dem / am Deutschkurs'."
|
||||
},
|
||||
{
|
||||
id: "prep5",
|
||||
verb: "abhängen",
|
||||
preposition: "von",
|
||||
grammaticalCase: "Dativ",
|
||||
daForm: "davon",
|
||||
woForm: "wovon",
|
||||
english: "to depend on",
|
||||
sentenceBefore: "Ob wir im Park grillen können, hängt natürlich stark",
|
||||
sentenceAfter: "Wetter ab.",
|
||||
nounHint: "das Wetter (neut.)",
|
||||
correctCombo: "vom",
|
||||
acceptedAnswers: ["vom", "von dem"],
|
||||
options: ["vom (von dem)", "auf das", "über das", "mit dem"],
|
||||
daExampleDe: "Kommst du heute Abend mit? — Das hängt davon ab, wann ich Feierabend habe.",
|
||||
explanation: "'abhängen' takes 'von + Dativ' (von dem Wetter = vom Wetter). Remember: 'von' is ALWAYS Dative in German!"
|
||||
},
|
||||
{
|
||||
id: "prep6",
|
||||
verb: "warten",
|
||||
preposition: "auf",
|
||||
grammaticalCase: "Akkusativ",
|
||||
daForm: "darauf",
|
||||
woForm: "worauf",
|
||||
english: "to wait for",
|
||||
sentenceBefore: "Auf dem Bahnsteig warten wir schon seit zwanzig Minuten",
|
||||
sentenceAfter: "Zug nach Hamburg.",
|
||||
nounHint: "der Zug (mask.)",
|
||||
correctCombo: "auf den",
|
||||
acceptedAnswers: ["auf den"],
|
||||
options: ["auf den", "für den", "auf dem", "nach dem"],
|
||||
daExampleDe: "Mein neuer Reisepass ist noch nicht da; ich warte schon drei Wochen darauf.",
|
||||
explanation: "English says 'wait FOR', but German NEVER uses 'für' with warten! It is always 'warten AUF + Akkusativ' ('auf den Zug')."
|
||||
},
|
||||
{
|
||||
id: "prep7",
|
||||
verb: "sich gewöhnen",
|
||||
preposition: "an",
|
||||
grammaticalCase: "Akkusativ",
|
||||
daForm: "daran",
|
||||
woForm: "woran",
|
||||
english: "to get used to / accustomed to",
|
||||
sentenceBefore: "Nach ein paar Monaten habe ich mich schnell",
|
||||
sentenceAfter: "ruhigen Sonntage in Deutschland gewöhnt.",
|
||||
nounHint: "die Sonntage (Plural)",
|
||||
correctCombo: "an die",
|
||||
acceptedAnswers: ["an die"],
|
||||
options: ["an die", "zu den", "mit den", "an den"],
|
||||
daExampleDe: "Am Anfang war die deutsche Bürokratie ungewohnt, aber man gewöhnt sich daran.",
|
||||
explanation: "'sich gewöhnen' takes 'an + Akkusativ' (moving toward a new habit → Accusative 'an die Sonntage')."
|
||||
},
|
||||
{
|
||||
id: "prep8",
|
||||
verb: "bitten",
|
||||
preposition: "um",
|
||||
grammaticalCase: "Akkusativ",
|
||||
daForm: "darum",
|
||||
woForm: "worum",
|
||||
english: "to ask for / request politely",
|
||||
sentenceBefore: "Wegen der Verspätung bitten wir alle Fahrgäste",
|
||||
sentenceAfter: "Verständnis.",
|
||||
nounHint: "das Verständnis (neut.)",
|
||||
correctCombo: "um das",
|
||||
acceptedAnswers: ["um", "um das", "um ihr"],
|
||||
options: ["um das (um Ihr)", "für das", "nach dem", "über das"],
|
||||
daExampleDe: "Kannst du mir beim Umzug helfen? Ich wollte dich schon gestern darum bitten.",
|
||||
explanation: "'bitten UM + Akkusativ' means 'to ask FOR something' ('Wir bitten um Ihr Verständnis'). Compare with 'fragen nach + Dativ' (to ask about information)."
|
||||
},
|
||||
{
|
||||
id: "prep9",
|
||||
verb: "sich interessieren",
|
||||
preposition: "für",
|
||||
grammaticalCase: "Akkusativ",
|
||||
daForm: "dafür",
|
||||
woForm: "wofür",
|
||||
english: "to be interested in",
|
||||
sentenceBefore: "Bei der Besichtigung haben sich über dreißig Personen",
|
||||
sentenceAfter: "Wohnung interessiert.",
|
||||
nounHint: "die Wohnung (fem.)",
|
||||
correctCombo: "für die",
|
||||
acceptedAnswers: ["für die", "fuer die"],
|
||||
options: ["für die", "in der", "an der", "über die"],
|
||||
daExampleDe: "Wofür interessierst du dich besonders? — Für deutsche Geschichte und Kultur!",
|
||||
explanation: "English says 'interested IN', but German says 'sich interessieren FÜR + Akkusativ' ('für die Wohnung')."
|
||||
},
|
||||
{
|
||||
id: "prep10",
|
||||
verb: "sich erinnern",
|
||||
preposition: "an",
|
||||
grammaticalCase: "Akkusativ",
|
||||
daForm: "daran",
|
||||
woForm: "woran",
|
||||
english: "to remember / recall",
|
||||
sentenceBefore: "Erinnerst du dich noch",
|
||||
sentenceAfter: "unseren ersten Ausflug nach München?",
|
||||
nounHint: "der Ausflug (mask., 'unseren' = Akk.)",
|
||||
correctCombo: "an",
|
||||
acceptedAnswers: ["an"],
|
||||
options: ["an", "über", "von", "auf"],
|
||||
daExampleDe: "Ja, natürlich erinnere ich mich noch sehr gut daran!",
|
||||
explanation: "'sich erinnern AN + Akkusativ' = to remember. Notice how 'daran' replaces 'an unseren ersten Ausflug'."
|
||||
}
|
||||
];
|
||||
+106
-22
@@ -7,7 +7,7 @@
|
||||
<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=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="styles.css?v=4" />
|
||||
<link rel="stylesheet" href="styles.css?v=5" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -30,9 +30,18 @@
|
||||
<span>📊</span> Übersicht
|
||||
</button>
|
||||
<button class="nav-tab" data-view="trainer" onclick="app.switchView('trainer')">
|
||||
<span>⚡</span> Wortschatz-Trainer
|
||||
<span>⚡</span> Trainer
|
||||
<span class="nav-badge" id="nav-due-badge">0</span>
|
||||
</button>
|
||||
<button class="nav-tab" data-view="builder" onclick="app.switchView('builder')">
|
||||
<span>🧱</span> Wortbaukasten
|
||||
</button>
|
||||
<button class="nav-tab" data-view="prepositions" onclick="app.switchView('prepositions')">
|
||||
<span>🎯</span> Präpositionen
|
||||
</button>
|
||||
<button class="nav-tab" data-view="listening" onclick="app.switchView('listening')">
|
||||
<span>🎧</span> Hörtraining
|
||||
</button>
|
||||
<button class="nav-tab" data-view="reader" onclick="app.switchView('reader')">
|
||||
<span>📖</span> Leseecke
|
||||
</button>
|
||||
@@ -68,7 +77,7 @@
|
||||
<p>
|
||||
After 1,400 days on Duolingo, you already have a strong intuition for basic German structure.
|
||||
<strong>WortKraft</strong> bridges the A2→B1 plateau by training <em>active recall with articles</em>,
|
||||
<em>compound word logic</em>, and <em>real-world production</em>.
|
||||
<em>compound word logic</em>, <em>verb + preposition cases</em>, and <em>native listening & production</em>.
|
||||
</p>
|
||||
<div class="gender-legend-bar">
|
||||
<span class="gender-badge gender-der">der • Maskulin</span>
|
||||
@@ -81,8 +90,8 @@
|
||||
<button class="btn btn-primary" onclick="app.switchView('trainer')">
|
||||
⚡ Jetzt trainieren (<span id="hero-due-count">0</span> fällig)
|
||||
</button>
|
||||
<button class="btn btn-light" onclick="app.switchView('reader')">
|
||||
📖 Geschichte lesen
|
||||
<button class="btn btn-light" onclick="app.switchView('builder')">
|
||||
🧱 Wortbaukasten
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -99,26 +108,26 @@
|
||||
<div class="stat-sub" id="stat-mastered-sub">0 gemeistert • 0 im Training</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Artikel-Trefferquote</div>
|
||||
<div class="stat-label">Artikel- & Übungsquote</div>
|
||||
<div class="stat-value" id="stat-accuracy">—</div>
|
||||
<div class="stat-sub">der / die / das & aktive Eingabe</div>
|
||||
<div class="stat-sub">der / die / das & Präpositionen</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Aktive Sätze geschrieben</div>
|
||||
<div class="stat-label">Aktive Sätze produziert</div>
|
||||
<div class="stat-value" id="stat-sentences">0</div>
|
||||
<div class="stat-sub">In der Sprachpraxis produziert</div>
|
||||
<div class="stat-sub">Sprachpraxis & Hörtraining</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-columns">
|
||||
<div class="panel-card">
|
||||
<div class="panel-header">
|
||||
<h2 class="panel-title">Dein täglicher 3-Schritte-Workflow</h2>
|
||||
<h2 class="panel-title">Deine 6 Trainings-Module (A2 → B1)</h2>
|
||||
</div>
|
||||
<div class="module-launcher-list">
|
||||
<div class="launcher-item" onclick="app.startTrainerMode('cloze')">
|
||||
<div class="launcher-left">
|
||||
<div class="launcher-icon">🎯</div>
|
||||
<div class="launcher-icon">⚡</div>
|
||||
<div>
|
||||
<div class="launcher-title">1. Aktiver Abruf im Kontext (SRS-Trainer)</div>
|
||||
<div class="launcher-desc">Lückensätze, Englisch → Deutsch mit Artikel & Plural-Drill statt Multiple-Choice.</div>
|
||||
@@ -127,12 +136,45 @@
|
||||
<span>→</span>
|
||||
</div>
|
||||
|
||||
<div class="launcher-item" onclick="app.switchView('builder')">
|
||||
<div class="launcher-left">
|
||||
<div class="launcher-icon">🧱</div>
|
||||
<div>
|
||||
<div class="launcher-title">2. Wortbaukasten (Komposita & Präfix-Labor)</div>
|
||||
<div class="launcher-desc">Verstehe trennbare/untrennbare Verben (nehmen, stellen, kommen) & lange zusammengesetzte Nomen.</div>
|
||||
</div>
|
||||
</div>
|
||||
<span>→</span>
|
||||
</div>
|
||||
|
||||
<div class="launcher-item" onclick="app.switchView('prepositions')">
|
||||
<div class="launcher-left">
|
||||
<div class="launcher-icon">🎯</div>
|
||||
<div>
|
||||
<div class="launcher-title">3. Verben mit Präposition & Kasus (Rektion + Da-/Wo-)</div>
|
||||
<div class="launcher-desc">Meistere warten auf (+Akk), teilnehmen an (+Dat), sich kümmern um (+Akk) & darauf/worauf.</div>
|
||||
</div>
|
||||
</div>
|
||||
<span>→</span>
|
||||
</div>
|
||||
|
||||
<div class="launcher-item" onclick="app.switchView('listening')">
|
||||
<div class="launcher-left">
|
||||
<div class="launcher-icon">🎧</div>
|
||||
<div>
|
||||
<div class="launcher-title">4. Hörtraining & Aussprache-Check (Diktat & Mikrofon)</div>
|
||||
<div class="launcher-desc">Höre echte Alltagssätze ohne Text, tippe was du hörst, oder sprich den Satz selbst ins Mikrofon.</div>
|
||||
</div>
|
||||
</div>
|
||||
<span>→</span>
|
||||
</div>
|
||||
|
||||
<div class="launcher-item" onclick="app.switchView('reader')">
|
||||
<div class="launcher-left">
|
||||
<div class="launcher-icon">📖</div>
|
||||
<div>
|
||||
<div class="launcher-title">2. Interaktive Leseecke (A2/B1 Geschichten & Alltag)</div>
|
||||
<div class="launcher-desc">Klicke auf jedes Wort für Artikel, Stammform, Audio & 1-Klick-Speichern ins SRS-Deck.</div>
|
||||
<div class="launcher-title">5. Interaktive Leseecke (A2/B1 Geschichten & Alltag)</div>
|
||||
<div class="launcher-desc">Hover-Übersetzer + Klick auf jedes Wort für Artikel, Stammform, Audio & 1-Klick-SRS-Speichern.</div>
|
||||
</div>
|
||||
</div>
|
||||
<span>→</span>
|
||||
@@ -142,7 +184,7 @@
|
||||
<div class="launcher-left">
|
||||
<div class="launcher-icon">💬</div>
|
||||
<div>
|
||||
<div class="launcher-title">3. Sprachpraxis mit Zielwörtern</div>
|
||||
<div class="launcher-title">6. Sprachpraxis mit Zielwörtern</div>
|
||||
<div class="launcher-desc">Löse echte Alltagssituationen (Arzt, Bürgeramt, Feierabend) und nutze gezielt 5 Zielwörter.</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -197,7 +239,48 @@
|
||||
<div id="trainer-stage-container"></div>
|
||||
</section>
|
||||
|
||||
<!-- 3. INTERACTIVE READER (LESEECKE) -->
|
||||
<!-- 3. WORTBAUKASTEN (COMPOUND WORD & PREFIX LAB) -->
|
||||
<section id="view-builder" class="view-section">
|
||||
<div class="trainer-controls">
|
||||
<div class="mode-pills">
|
||||
<button class="mode-pill active" id="builder-tab-prefixes" onclick="app.setBuilderSubmode('prefixes')">
|
||||
🔀 Verb-Stämme & Präfix-Labor (trennbar vs. untrennbar)
|
||||
</button>
|
||||
<button class="mode-pill" id="builder-tab-compounds" onclick="app.setBuilderSubmode('compounds')">
|
||||
🧱 Komposita-Knacker (Zusammengesetzte Nomen)
|
||||
</button>
|
||||
<button class="mode-pill" id="builder-tab-quiz" onclick="app.setBuilderSubmode('quiz')">
|
||||
⚡ Präfix- & Komposita-Quiz
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="builder-stage-container"></div>
|
||||
</section>
|
||||
|
||||
<!-- 4. VERBEN MIT PRÄPOSITION (REKTION & DA-/WO- TRAINER) -->
|
||||
<section id="view-prepositions" class="view-section">
|
||||
<div class="trainer-controls">
|
||||
<div class="mode-pills">
|
||||
<button class="mode-pill active" id="prep-tab-drill" onclick="app.setPrepSubmode('drill')">
|
||||
🎯 Präposition + Kasus-Trainer (auf den / am / um das)
|
||||
</button>
|
||||
<button class="mode-pill" id="prep-tab-dawo" onclick="app.setPrepSubmode('dawo')">
|
||||
🔗 Da- & Wo-Baukasten (darauf, worauf, darum, daran)
|
||||
</button>
|
||||
<button class="mode-pill" id="prep-tab-reference" onclick="app.setPrepSubmode('reference')">
|
||||
📋 Übersichtstabelle (Akkusativ vs. Dativ)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="prepositions-stage-container"></div>
|
||||
</section>
|
||||
|
||||
<!-- 5. HÖRTRAINING & AUSSPRACHE (LISTENING DICTATION & SHADOWING) -->
|
||||
<section id="view-listening" class="view-section">
|
||||
<div id="listening-stage-container"></div>
|
||||
</section>
|
||||
|
||||
<!-- 6. INTERACTIVE READER (LESEECKE) -->
|
||||
<section id="view-reader" class="view-section">
|
||||
<div class="reader-layout">
|
||||
<aside class="story-sidebar">
|
||||
@@ -216,7 +299,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 4. CONVERSATION & WRITING PRACTICE (SPRACHPRAXIS) -->
|
||||
<!-- 7. CONVERSATION & WRITING PRACTICE (SPRACHPRAXIS) -->
|
||||
<section id="view-practice" class="view-section">
|
||||
<div class="scenario-layout">
|
||||
<aside class="story-sidebar">
|
||||
@@ -231,7 +314,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 5. VOCABULARY BANK (WÖRTERBUCH) -->
|
||||
<!-- 8. VOCABULARY BANK (WÖRTERBUCH) -->
|
||||
<section id="view-dictionary" class="view-section">
|
||||
<div class="panel-header" style="margin-bottom: 1.25rem; flex-wrap: wrap;">
|
||||
<div>
|
||||
@@ -408,10 +491,11 @@
|
||||
<div id="toast" class="toast-notification"></div>
|
||||
|
||||
<!-- SCRIPTS -->
|
||||
<script src="data/vocabulary.js?v=4"></script>
|
||||
<script src="data/stories.js?v=4"></script>
|
||||
<script src="data/scenarios.js?v=4"></script>
|
||||
<script src="data/hover_dict.js?v=4"></script>
|
||||
<script src="app.js?v=4"></script>
|
||||
<script src="data/vocabulary.js?v=5"></script>
|
||||
<script src="data/stories.js?v=5"></script>
|
||||
<script src="data/scenarios.js?v=5"></script>
|
||||
<script src="data/hover_dict.js?v=5"></script>
|
||||
<script src="data/labs.js?v=5"></script>
|
||||
<script src="app.js?v=5"></script>
|
||||
</body>
|
||||
</html>
|
||||
+137
@@ -1125,3 +1125,140 @@ body.hover-translate-on .word-token:hover {
|
||||
margin-top: 0.15rem;
|
||||
}
|
||||
|
||||
/* === WORTBAUKASTEN (COMPOUND & PREFIX LAB) === */
|
||||
.compound-blocks-row {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.compound-block {
|
||||
background: var(--bg-main);
|
||||
border: 2px solid var(--border-strong);
|
||||
border-radius: 12px;
|
||||
padding: 0.75rem 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
min-width: 130px;
|
||||
}
|
||||
|
||||
.compound-block.head-word {
|
||||
border-color: var(--primary);
|
||||
background: #fffbeb;
|
||||
box-shadow: 0 0 0 3px rgba(217, 119, 6, 0.12);
|
||||
}
|
||||
|
||||
.compound-plus {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: 800;
|
||||
font-size: 1.25rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.prefix-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(290px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.prefix-card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 1.15rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.sep-pill {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
padding: 0.15rem 0.55rem;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.sep-yes {
|
||||
background: #ecfdf5;
|
||||
color: #047857;
|
||||
border: 1px solid #6ee7b7;
|
||||
}
|
||||
|
||||
.sep-no {
|
||||
background: #eff6ff;
|
||||
color: #1d4ed8;
|
||||
border: 1px solid #93c5fd;
|
||||
}
|
||||
|
||||
/* === HÖRTRAINING (LISTENING DICTATION & SHADOWING) === */
|
||||
.audio-hero-box {
|
||||
background: linear-gradient(135deg, #1e1b4b 0%, #312e81 100%);
|
||||
color: #fff;
|
||||
border-radius: var(--radius-md);
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.diff-word {
|
||||
display: inline-block;
|
||||
padding: 0.15rem 0.45rem;
|
||||
border-radius: 6px;
|
||||
margin: 0.12rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.diff-ok {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.diff-miss {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
text-decoration: underline wavy;
|
||||
}
|
||||
|
||||
.mic-btn-recording {
|
||||
background: #dc2626 !important;
|
||||
color: #fff !important;
|
||||
animation: pulseMic 1.2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulseMic {
|
||||
0% { box-shadow: 0 0 0 0 rgba(220, 38, 38, 0.55); }
|
||||
70% { box-shadow: 0 0 0 10px rgba(220, 38, 38, 0); }
|
||||
100% { box-shadow: 0 0 0 0 rgba(220, 38, 38, 0); }
|
||||
}
|
||||
|
||||
/* === PREPOSITION & CASE TRAINER === */
|
||||
.case-badge-akk {
|
||||
background: #fef3c7;
|
||||
color: #b45309;
|
||||
border: 1px solid #fcd34d;
|
||||
font-weight: 700;
|
||||
font-size: 0.76rem;
|
||||
padding: 0.15rem 0.55rem;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.case-badge-dat {
|
||||
background: #ede9fe;
|
||||
color: #6d28d9;
|
||||
border: 1px solid #c4b5fd;
|
||||
font-weight: 700;
|
||||
font-size: 0.76rem;
|
||||
padding: 0.15rem 0.55rem;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user