diff --git a/src/App.jsx b/src/App.jsx index cd10a1f..6c92730 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -60,10 +60,10 @@ const TABS = [ {id:'pronoun', label:'Pronouns'}, ]; -// Daily Practice pseudo-level: pinned to the top of the Prepositions tab once -// the "Alle Präpositionen" lesson (id 10) is completed. Always free. -const DAILY_ID = 10.5; -const FREE_LEVEL_IDS = new Set([2, 3, 4, 8, 9, 10, 10.5, 18]); +// 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 CORE_ARTICLE_CASES = ['nominative', 'accusative', 'dative']; const PAYMENTS_ENABLED = import.meta.env.VITE_PAYMENTS_ENABLED === 'true'; const STRIPE_PLANS = PAYMENTS_ENABLED ? [ @@ -231,6 +231,8 @@ export default function App() { const startLv = id => { if (!unlocked(id)) { setPayMsg(''); setScr('paywall'); return; } + const lvObj = LEVELS.find(l => l.id === id); + if (lvObj?.tab) setTab(lvObj.tab); sessionRef.current = createSession(id); setLv(id); setRes([]); setInp(''); setRev(false); setQ(sessionRef.current.next()); setScr('practice'); }; @@ -245,6 +247,7 @@ export default function App() { const submitRef = useRef(null); const nextRef = useRef(null); + const summaryRef = useRef(null); const doSubmit = useCallback((overrideInput) => { const userAns = (overrideInput ?? inp).trim(); @@ -277,7 +280,7 @@ export default function App() { useEffect(() => { const h = e => { if (chart) { if (e.key === 'Escape') setChart(false); return; } - if (scr === 'summary' && e.key === 'Enter') { e.preventDefault(); startLv(lv); return; } + if (scr === 'summary' && e.key === 'Enter') { e.preventDefault(); summaryRef.current?.(); return; } if (scr !== 'practice') return; if (!rev && (q?.type === 'article' || q?.type === 'chunk') && q.choices) { const idx = ['1','2','3','4'].indexOf(e.key); @@ -300,14 +303,15 @@ export default function App() { return () => window.removeEventListener('keydown', h); }); - // One-tap mode: after an answer is revealed, auto-advance to the next - // question. Always on in Daily Practice; opt-in (st.auto) elsewhere. Correct - // answers flash briefly; wrong answers linger so the correction can register. + // One-tap mode: always on in Daily Practice, opt-in (st.auto) elsewhere. + // A correct answer never shows the solution — just a green tick, then it + // skips straight to the next question. A wrong answer shows the correction + // and waits for an explicit Next, so it can actually register. useEffect(() => { - if (scr !== 'practice' || !rev) return; - const quick = lv === DAILY_ID || !!st.auto; + if (scr !== 'practice' || !rev || !ok) return; + const quick = !!LEVELS.find(l => l.id === lv)?.daily || !!st.auto; if (!quick) return; - const t = setTimeout(() => { nextRef.current?.(); }, ok ? 750 : 1650); + const t = setTimeout(() => { nextRef.current?.(); }, 550); return () => clearTimeout(t); }, [scr, rev, ok, lv, st.auto]); @@ -317,13 +321,28 @@ export default function App() { 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 nextInTab = nextLevelInTab(lv); - const quickActive = lv === DAILY_ID || !!st.auto; - // Daily Practice unlocks once the all-31 lesson (id 10) is passed: - // at least one full round with the app's 70% pass rate. - const prepMastery = st.sc?.[10]; - const dailyUnlocked = !!(prepMastery && prepMastery.t >= ROUND && prepMastery.c / prepMastery.t >= 0.7); - const dailySc = st.sc?.[DAILY_ID]; - const dailyPct = dailySc ? Math.round(dailySc.c / dailySc.t * 100) : null; + const quickActive = !!cur?.daily || !!st.auto; + const quickTick = quickActive && rev && ok; + // Each tab's Daily Practice unlocks once every lesson in the tab is passed: + // at least one full round each, at the app's 70% pass rate. + const passedLv = id => { const s = st.sc?.[id]; return !!(s && s.t >= ROUND && s.c / s.t >= 0.7); }; + const tabLessons = t => LEVELS.filter(l => l.tab === t && !l.daily); + const dailyFor = t => LEVELS.find(l => l.tab === t && l.daily); + const dailyUnlockedFor = t => tabLessons(t).every(l => passedLv(l.id)); + // The next tab (cycling through the top tabs) with an unlocked Daily Practice. + const nextDailyAfter = t => { + const i = TABS.findIndex(x => x.id === t); + for (let k = 1; k < TABS.length; k++) { + const cand = TABS[(i + k) % TABS.length]; + if (dailyUnlockedFor(cand.id)) return { tab: cand, level: dailyFor(cand.id) }; + } + return null; + }; + const nextDaily = cur?.daily ? nextDailyAfter(cur.tab) : null; + summaryRef.current = () => { + if (cur?.daily && nextDaily) startLv(nextDaily.level.id); + else startLv(lv); + }; return (