Feedback: show the full declension table, not one row
Every answer card now renders the complete 4×4 declension grid instead of a single case row. The correct cell is highlighted green; on a wrong answer, the cell matching what you typed is highlighted red, so you see both your pick and the right one in context. - FillFeedback (articles + possessives): full table, correct + wrong highlighting within the noun's gender column. - ChunkFeedback + ContractionFeedback: contraction questions now show the definite-article table too, not just the fused answer. - PronounFeedback: correct cell green, your wrong pick red.
This commit is contained in:
+76
-35
@@ -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 (
|
||||
<table style={S.stripTbl}>
|
||||
<thead><tr>
|
||||
<th style={{...S.stripH, textAlign:'left'}}></th>
|
||||
{FB_GENDERS.map(g => (
|
||||
<th key={g} style={{...S.stripH, color: GENDER_C[g], opacity: (!focusG || g === focusG) ? 1 : 0.5}}>{g.slice(0,4)}.</th>
|
||||
))}
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{FB_CASES.map(c => (
|
||||
<tr key={c}>
|
||||
<td style={{...S.stripD, textAlign:'left', fontSize:11, fontWeight:600, color: CASE_C[c]}}>{CL[c]}</td>
|
||||
{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 (
|
||||
<td key={g} style={{...S.stripD, background:bg, borderRadius:4,
|
||||
fontWeight:(correct||wrong)?700:400, color, fontSize:(correct||wrong)?14:13}}>{val}</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div style={{...S.feedback, animation:'fadeUp 0.2s ease-out'}}>
|
||||
@@ -918,24 +951,12 @@ function FillFeedback({ q, inp, ok }) {
|
||||
{q.noun.w} is <span style={{color:GENDER_C[q.noun.g]}}>{q.noun.g}</span>. <span style={{color:CASE_C[q.cas]}}>{CL[q.cas]}</span> + {q.noun.g} = <strong style={{color:'#1a1a1a'}}>{q.ans}</strong>
|
||||
</div>
|
||||
)}
|
||||
<table style={S.stripTbl}>
|
||||
<thead><tr>
|
||||
<th style={{...S.stripH, textAlign:'left', fontWeight:600, color: CASE_C[q.cas]}}>{CL[q.cas]}</th>
|
||||
{genders.map(g => <th key={g} style={{...S.stripH, color:GENDER_C[g]}}>{g.slice(0,4)}.</th>)}
|
||||
</tr></thead>
|
||||
<tbody><tr>
|
||||
<td style={{...S.stripD, textAlign:'left', color:'#aaa', fontSize:11}}>{stripLabel}</td>
|
||||
{genders.map((g, i) => {
|
||||
const hl = g === q.noun.g;
|
||||
return <td key={g} style={{
|
||||
...S.stripD, fontWeight: hl ? 700 : 400,
|
||||
color: hl ? '#1a1a1a' : '#bbb',
|
||||
background: hl ? '#f3f3f3' : 'transparent',
|
||||
borderRadius: 4, fontSize: hl ? 15 : 13,
|
||||
}}>{stripCells[i]}</td>;
|
||||
})}
|
||||
</tr></tbody>
|
||||
</table>
|
||||
<FeedbackTable
|
||||
valueFor={valueFor}
|
||||
focusG={q.noun.g}
|
||||
isCorrect={(g, c) => g === q.noun.g && c === q.cas}
|
||||
isWrong={ok ? null : (g, c, val) => g === q.noun.g && normAns(val) === wrong}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1002,6 +1023,15 @@ function ChunkFeedback({ q, inp, ok }) {
|
||||
? 'Preferred — these fuse, so use the contraction in speech and writing.'
|
||||
: <><strong>{article}</strong> doesn’t fuse with „{q.prepKey}" — keep them as two words.</>}
|
||||
</div>
|
||||
<div style={{marginTop:10, fontSize:11, color:'#aaa', marginBottom:2}}>
|
||||
Definite article by case — <span style={{color:GENDER_C[q.noun.g]}}>{DEF[q.noun.g].nominative} {q.noun.w}</span>
|
||||
</div>
|
||||
<FeedbackTable
|
||||
valueFor={(g, c) => DEF[g][c]}
|
||||
focusG={q.noun.g}
|
||||
isCorrect={(g, c) => g === q.noun.g && c === q.cas}
|
||||
isWrong={null}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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.'}
|
||||
</div>
|
||||
<div style={{marginTop:10, fontSize:11, color:'#aaa', marginBottom:2}}>
|
||||
Where „{q.art}" sits in the definite-article table
|
||||
</div>
|
||||
<FeedbackTable
|
||||
valueFor={(g, c) => DEF[g][c]}
|
||||
focusG={null}
|
||||
isCorrect={(g, c, val) => normAns(val) === normAns(q.art)}
|
||||
isWrong={null}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1046,13 +1085,15 @@ function PronounFeedback({ q, inp, ok }) {
|
||||
<tbody><tr>
|
||||
<td style={{...S.stripD, textAlign:'left', color:'#aaa', fontSize:11}}>{q.pron.en}</td>
|
||||
{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 <td key={c} style={{
|
||||
...S.stripD, fontWeight: hl ? 700 : 400,
|
||||
color: hl ? '#1a1a1a' : '#bbb',
|
||||
background: hl ? '#f3f3f3' : 'transparent',
|
||||
borderRadius: 4, fontSize: hl ? 15 : 13,
|
||||
}}>{q.pron[keys[c]]}</td>;
|
||||
...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}</td>;
|
||||
})}
|
||||
</tr></tbody>
|
||||
</table>
|
||||
|
||||
Reference in New Issue
Block a user