diff --git a/src/App.jsx b/src/App.jsx index 87ccfd3..37c6b30 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -3,6 +3,7 @@ import { DEF, INDEF, STRONG, WEAK, PREPS, PREPS_BY_CASE, CL, CASE_C, GENDER_C, PRONOUNS, POSSESSIVES, POSS_ENDINGS, combinePoss, LEVELS, wechselExample, createSession, bumpStreak, currentStreak, weekDots, + nounForm, ADJ_ENDINGS, ADJ_CLASS_LABEL, ADJ_CLASS_HINT, DER_ENDINGS, } from './engine.js'; // ═══════════════════════════════════════════ @@ -58,12 +59,15 @@ const TABS = [ {id:'article', label:'Articles'}, {id:'poss', label:'Possessives'}, {id:'pronoun', label:'Pronouns'}, + {id:'adj', label:'Adjectives'}, ]; // Daily Practice pseudo-levels are always free: unlocking one requires // passing every lesson in its tab, so gated tabs stay gated anyway. const DAILY_IDS = LEVELS.filter(l => l.daily).map(l => l.id); const FREE_LEVEL_IDS = new Set([2, 3, 4, 8, 9, 10, 18, ...DAILY_IDS]); +const ADJ_GENDERS = ['masculine','feminine','neuter','plural']; +const ADJ_CASES = ['nominative','accusative','dative','genitive']; const CORE_ARTICLE_CASES = ['nominative', 'accusative', 'dative']; const PAYMENTS_ENABLED = import.meta.env.VITE_PAYMENTS_ENABLED === 'true'; const STRIPE_PLANS = PAYMENTS_ENABLED ? [ @@ -185,6 +189,7 @@ export default function App() { const [chart, setChart] = useState(false); const [rule, setRule] = useState(false); const [pickPrep, setPickPrep] = useState(null); + const [nounInfo, setNounInfo] = useState(null); const [learnStep, setLearnStep] = useState(0); const [st, setSt] = useState({sc:{},tc:0,ta:0,streak:0,ld:null}); const [access, setAccess] = useState(null); @@ -287,6 +292,7 @@ export default function App() { useEffect(() => { const h = e => { + if (nounInfo) { if (e.key === 'Escape') setNounInfo(null); return; } if (rule) { if (e.key === 'Escape') setRule(false); return; } if (chart) { if (e.key === 'Escape') setChart(false); return; } if (scr === 'summary' && e.key === 'Enter') { e.preventDefault(); summaryRef.current?.(); return; } @@ -339,7 +345,7 @@ export default function App() { const rc = res.filter(Boolean).length; const cur = LEVELS.find(l=>l.id===lv); - const chartSection = scr === 'menu' ? tab : (q?.type === 'wechselContext' ? 'prep' : q?.type === 'chunk' ? 'article' : (q?.type || tab)); + const chartSection = scr === 'menu' ? tab : (q?.type === 'wechselContext' ? 'prep' : q?.type === 'chunk' ? 'article' : (q?.type === 'adjective' || q?.type === 'adjClass') ? 'adj' : (q?.type || tab)); const nextInTab = nextLevelInTab(lv); const quickActive = !!cur?.daily || !!st.auto; const quickTick = quickActive && rev && ok; @@ -598,6 +604,14 @@ export default function App() { {q.type === 'poss' && <> · {DEF[q.noun.g].nominative} {q.noun.w}} + {nounForm(q.noun, q.cas) !== q.noun.w && ( +
+ +
+ )} + {!rev ? ( q.choices ? ( @@ -754,6 +768,72 @@ export default function App() { )} + {/* PRACTICE: adjective endings */} + {scr === 'practice' && (q?.type === 'adjective' || q?.type === 'adjClass') && ( +
+
+ {q.sentence.split('_____').map((part,i,a)=>( + + {part} + {i < a.length-1 && ( + + {rev ? ADJ_ENDINGS[q.cls][q.noun.g][q.cas] : ' '} + + )} + + ))} +
+ + + + + + + +
+ {q.type === 'adjClass' ? 'Which ending table?' : 'Which ending?'} + + {nounForm(q.noun, q.cas) !== q.noun.w && ( + + )} +
+ + {!rev ? ( + q.type === 'adjClass' ? ( +
+ {q.choices.map((c,i) => ( + + ))} +
+ ) : ( + '-' + c)} onPick={c => doSubmit(c.slice(1))}/> + ) + ) : quickTick ? ( + + ) : ( + setRule('adj')}/> + )} +
+ )} + {scr === 'practice' && (
@@ -823,8 +903,9 @@ export default function App() { {scr !== 'practice' && }
- {chart && setChart(false)} onRule={()=>{ setChart(false); setRule(true); }}/>} - {rule && setRule(false)}/>} + {chart && setChart(false)} onRule={kind => { setChart(false); setRule(kind || true); }}/>} + {rule && (rule === 'adj' ? setRule(false)}/> : setRule(false)}/>)} + {nounInfo && setNounInfo(null)}/>}
); @@ -1165,6 +1247,183 @@ function WechselContextFeedback({ q, inp, ok, onRule }) { ); } +function AdjectiveFeedback({ q, inp, ok, onRule }) { + const ending = ADJ_ENDINGS[q.cls][q.noun.g][q.cas]; + const isClassQ = q.type === 'adjClass'; + const gotLabel = isClassQ ? (ADJ_CLASS_LABEL[inp] || inp) : '-' + inp; + const wantLabel = isClassQ ? ADJ_CLASS_LABEL[q.cls] : '-' + ending; + return ( +
+
+ {ok + ? <>✓ {wantLabel} + : <>✗ {gotLabel} → {wantLabel}} +
+
+ {q.answerSentence} +
+
+ {q.det + ? <>„{q.det}" is {q.cls === 'weak' ? 'a der-word' : 'an ein-word'} → {ADJ_CLASS_LABEL[q.cls]} + : <>No determiner → the adjective has to carry the case → Stark} + {' · '}{CL[q.cas]} + {' '}{q.noun.g} + {' → '}-{ending} +
+
+ {ADJ_CLASS_LABEL[q.cls]} — {ADJ_CLASS_HINT[q.cls]} +
+ + +
+ ); +} + +function AdjTable({ cls, g, cas, wrong }) { + return ( + + + + {ADJ_GENDERS.map(x => )} + + + {ADJ_CASES.map(c => ( + + + {ADJ_GENDERS.map(x => { + const val = ADJ_ENDINGS[cls][x][c]; + const hit = x === g && c === cas; + const bad = !hit && wrong && x === g && val === wrong; + return ; + })} + + ))} + +
{x.slice(0,4)}.
{CL[c]}-{val}
+ ); +} + +// Why the noun itself changed — the last surviving case ending on German nouns. +function NounEndingModal({ q, onClose }) { + const changed = nounForm(q.noun, q.cas); + return ( +
+
e.stopPropagation()}> +
+
Why „{changed}"?
+ +
+
+ German normally leaves the noun alone and lets the article do the work. The + genitive is the one surviving exception: masculine and neuter nouns + add -s or -es in the genitive singular. So {q.noun.w} becomes + {changed}. +
+
+ {DEF[q.noun.g].genitive} {changed} — {q.noun.en} +
+ +
-es or just -s?
+
+ -es after one-syllable nouns, and after -s, -ß, -x, -z:
+ des Hauses, des Mannes, des Kindes, des Platzes +
+
+ -s after longer nouns — especially those ending in -el, -en, -er — + and after foreign words:
+ des Regens, des Vaters, des Lehrers, des Autos, des Hotels +
+ +
Feminine nouns never change
+
+ der Frau, der Stadt, der Nacht — the article alone marks the case. + Same for plurals: der Kinder. +
+ +
The only other one left
+
+ The dative plural adds -n if the plural doesn't already end in one: + mit den Kindern, zwischen den Bäumen. +
+
+ There is also a small group of masculine nouns — der Junge, der Student, + der Herr, der Mensch — that take -n in every case except the nominative. + None of them appear in Beug yet. +
+ + +
+
+ ); +} + +// The three adjective tables and the single idea underneath them. +function AdjRuleModal({ onClose }) { + const rows = [ + ['weak', 'After a der-word', 'der · dieser · jeder · welcher · alle', + 'The determiner already shows the case, so the adjective goes limp — only -e or -en.', + 'der gut\u00ADe Mann · den guten Mann'], + ['mixed', 'After an ein-word', 'ein · kein · mein · dein · sein · ihr · unser · euer', + 'Same as weak, EXCEPT the three slots where the ein-word has no ending of its own. There the adjective steps in and takes the strong ending.', + 'ein guter Mann · ein gutes Kind'], + ['strong', 'No determiner', '— nothing in front —', + 'Nothing else can signal the case, so the adjective carries the full load.', + 'guter Wein · kaltes Wasser'], + ]; + return ( +
+
e.stopPropagation()}> +
+
Which ending table?
+ +
+ +
+ One idea holds the whole system together: the case and gender have to be + signalled exactly once in the noun phrase. Whoever is able to show it, + shows it — and whoever is left over relaxes. +
+ + {rows.map(([cls, title, words, body, ex]) => ( +
+
+ {ADJ_CLASS_LABEL[cls]} — {title} +
+
{words}
+
{body}
+
{ex}
+ +
+ ))} + +
Two things worth pinning down
+
+ Strong genitive masc/neut is -en, not -es — trotz schlechten + Wetters. The noun already carries the -(e)s, so the adjective doesn't repeat it. +
+
+ After the verb, an adjective takes no ending at all — + Das Haus ist groß, never großes. An adjective only declines + when it stands in front of its noun. +
+
+ Beug has no plural nouns yet, so the plural column is shown for reference but is + not drilled. +
+ + +
+
+ ); +} + // The wohin/wo rule, the trap, the verb pairs, and the one real exception. function WechselRuleModal({ onClose }) { const PAIRS = [ @@ -1453,7 +1712,7 @@ function ChartModal({ section, q, onClose, onRule }) { Wechsel: Akkusativ when the phrase is where something ends up (wohin?), Dativ when it is where the action happens (wo?) — movement alone does not decide it. - {onRule && } + {onRule && } )} @@ -1469,6 +1728,23 @@ function ChartModal({ section, q, onClose, onRule }) { )} + {section === 'adj' && ( +
+
Adjective Endings
+
+ The case is signalled once. If the determiner shows it, the adjective goes limp. +
+ {['weak','mixed','strong'].map(cls => ( +
+
{ADJ_CLASS_LABEL[cls]}
+
{ADJ_CLASS_HINT[cls]}
+ +
+ ))} + {onRule && } +
+ )} + {section === 'pronoun' && (
Personal Pronouns
@@ -1584,8 +1860,10 @@ const S = { pillBtnOn: { border:`1.5px solid ${GREEN}`, color:GREEN, background:'#f3ffe8' }, chartsPill: { position:'absolute', right:20, bottom:2, borderRadius:999, padding:'5px 13px', fontSize:12, fontWeight:600, cursor:'pointer', fontFamily:'inherit', lineHeight:1.2, border:'1.5px solid #d5d5d5', color:'#777', background:'#fff' }, - tabs: { display:'flex', gap:0, borderBottom:'1px solid #eee', marginTop:6, marginBottom:12 }, - tab: { flex:1, background:'none', border:'none', padding:'12px 8px', cursor:'pointer', color:'#999', fontSize:13, fontWeight:600, fontFamily:'inherit', borderBottomWidth:2, borderBottomStyle:'solid', borderBottomColor:'transparent', marginBottom:-1, transition:'all 0.15s' }, + // Five tabs no longer fit across a phone, so the row scrolls instead of + // squeezing the labels. The scrollbar itself is hidden (see the