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 => ( + + ))} + + + {FB_CASES.map(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 ( + + ); + })} + + ))} + +
{g.slice(0,4)}.
{CL[c]}{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}
)} - - - - {genders.map(g => )} - - - - {genders.map((g, i) => { - const hl = g === q.noun.g; - return ; - })} - -
{CL[q.cas]}{g.slice(0,4)}.
{stripLabel}{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}; })}