diff --git a/src/App.jsx b/src/App.jsx
index 41af2a6..cd10a1f 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -894,18 +894,51 @@ function PaywallScreen({ message, onCheckout, onBack }) {
// FEEDBACK COMPONENTS
// ═══════════════════════════════════════════
+const FB_GENDERS = ['masculine','feminine','neuter','plural'];
+const FB_CASES = ['nominative','accusative','dative','genitive'];
+const normAns = s => (s || '').trim().toLowerCase();
+
+// A full declension grid (all 4 cases × 4 genders) for answer feedback.
+// The correct cell is highlighted green; when the answer was wrong, the
+// cell(s) matching the user's input are highlighted red — so you see both
+// your pick and the right one in the whole table, not just one row.
+// focusG (optional) keeps one gender column sharp and greys the rest.
+function FeedbackTable({ valueFor, isCorrect, isWrong, focusG }) {
+ return (
+
+
+ |
+ {FB_GENDERS.map(g => (
+ {g.slice(0,4)}. |
+ ))}
+
+
+ {FB_CASES.map(c => (
+
+ | {CL[c]} |
+ {FB_GENDERS.map(g => {
+ const val = valueFor(g, c);
+ const correct = isCorrect(g, c, val);
+ const wrong = !correct && !!isWrong && isWrong(g, c, val);
+ const bg = correct ? GREEN + '22' : wrong ? RED + '22' : 'transparent';
+ const color = correct ? GREEN : wrong ? RED : (focusG && g !== focusG ? '#c8c8c8' : '#555');
+ return (
+ {val} |
+ );
+ })}
+
+ ))}
+
+
+ );
+}
+
function FillFeedback({ q, inp, ok }) {
- const genders = ['masculine','feminine','neuter','plural'];
- const stripCells = genders.map(g => {
- if (q.type === 'article') {
- const tbl = q.artType === 'indefinite' ? INDEF : DEF;
- return tbl[g][q.cas];
- }
- return combinePoss(q.stem, POSS_ENDINGS[g][q.cas]);
- });
- const stripLabel = q.type === 'article'
- ? (q.artType === 'indefinite' ? 'ein…' : 'der/die')
- : `${q.stem}-`;
+ const valueFor = q.type === 'article'
+ ? (g, c) => (q.artType === 'indefinite' ? INDEF : DEF)[g][c]
+ : (g, c) => combinePoss(q.stem, POSS_ENDINGS[g][c]);
+ const wrong = normAns(inp);
return (
@@ -918,24 +951,12 @@ function FillFeedback({ q, inp, ok }) {
{q.noun.w} is {q.noun.g}. {CL[q.cas]} + {q.noun.g} = {q.ans}
)}
-
-
- | {CL[q.cas]} |
- {genders.map(g => {g.slice(0,4)}. | )}
-
-
- | {stripLabel} |
- {genders.map((g, i) => {
- const hl = g === q.noun.g;
- return {stripCells[i]} | ;
- })}
-
-
+ g === q.noun.g && c === q.cas}
+ isWrong={ok ? null : (g, c, val) => g === q.noun.g && normAns(val) === wrong}
+ />
);
}
@@ -1002,6 +1023,15 @@ function ChunkFeedback({ q, inp, ok }) {
? 'Preferred — these fuse, so use the contraction in speech and writing.'
: <>{article} doesn’t fuse with „{q.prepKey}" — keep them as two words.>}
+
+ Definite article by case — {DEF[q.noun.g].nominative} {q.noun.w}
+
+ DEF[g][c]}
+ focusG={q.noun.g}
+ isCorrect={(g, c) => g === q.noun.g && c === q.cas}
+ isWrong={null}
+ />
);
}
@@ -1022,6 +1052,15 @@ function ContractionFeedback({ q, inp, ok }) {
? 'Preferred — use this contraction in both speech and writing.'
: 'Informal — common when speaking, but the two-word form is more usual in writing.'}
+
+ Where „{q.art}" sits in the definite-article table
+
+ DEF[g][c]}
+ focusG={null}
+ isCorrect={(g, c, val) => normAns(val) === normAns(q.art)}
+ isWrong={null}
+ />
);
}
@@ -1046,13 +1085,15 @@ function PronounFeedback({ q, inp, ok }) {
| {q.pron.en} |
{cases.map(c => {
- const hl = c === q.cas;
+ const val = q.pron[keys[c]];
+ const correct = c === q.cas;
+ const wrong = !ok && !correct && normAns(val) === normAns(inp);
return {q.pron[keys[c]]} | ;
+ ...S.stripD, fontWeight: (correct || wrong) ? 700 : 400,
+ color: correct ? GREEN : wrong ? RED : '#bbb',
+ background: correct ? GREEN + '22' : wrong ? RED + '22' : 'transparent',
+ borderRadius: 4, fontSize: (correct || wrong) ? 15 : 13,
+ }}>{val};
})}