From aa781b72bb9c08f009e5d4f964a598241fb69043 Mon Sep 17 00:00:00 2001 From: george Date: Fri, 17 Jul 2026 07:23:36 +0000 Subject: [PATCH] Add pinned Daily Practice card + one-tap mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Daily Practice (all 31 prepositions) is pinned to the top of the Prepositions tab once the "Alle Präpositionen" lesson is passed (>=70% over a full round). - One-tap mode: after answering, the app flashes right/wrong and auto-advances. Always on in Daily Practice; a persisted "One-tap" toggle in the practice header enables it for any other level. --- src/App.jsx | 104 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 72 insertions(+), 32 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 086d786..41af2a6 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -60,7 +60,10 @@ const TABS = [ {id:'pronoun', label:'Pronouns'}, ]; -const FREE_LEVEL_IDS = new Set([2, 3, 4, 8, 9, 10, 18]); +// 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]); const CORE_ARTICLE_CASES = ['nominative', 'accusative', 'dative']; const PAYMENTS_ENABLED = import.meta.env.VITE_PAYMENTS_ENABLED === 'true'; const STRIPE_PLANS = PAYMENTS_ENABLED ? [ @@ -220,7 +223,7 @@ export default function App() { const nextLevelInTab = (id) => { const lvObj = LEVELS.find(l => l.id === id); if (!lvObj) return null; - const tabLevels = LEVELS.filter(l => l.tab === lvObj.tab).sort((a,b) => a.id - b.id); + const tabLevels = LEVELS.filter(l => l.tab === lvObj.tab && !l.daily).sort((a,b) => a.id - b.id); const idx = tabLevels.findIndex(l => l.id === id); if (idx === -1 || idx === tabLevels.length - 1) return null; return tabLevels[idx + 1]; @@ -297,12 +300,30 @@ 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. + useEffect(() => { + if (scr !== 'practice' || !rev) return; + const quick = lv === DAILY_ID || !!st.auto; + if (!quick) return; + const t = setTimeout(() => { nextRef.current?.(); }, ok ? 750 : 1650); + return () => clearTimeout(t); + }, [scr, rev, ok, lv, st.auto]); + if (!loaded) return

Loading...

; 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 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; return (
@@ -314,6 +335,14 @@ export default function App() { ? :
Beug
}
+ {scr === 'practice' && ( + + )} {scr === 'practice' && } {PAYMENTS_ENABLED && hasPaidAccess && Unlocked} @@ -332,36 +361,47 @@ export default function App() { {scr === 'menu' && (
- {TABS.map(t => ( - - ))} + {TABS.map(t => ( + + ))}
+ {tab === 'prep' && dailyUnlocked && ( +
startLv(DAILY_ID)}> +
+
⚡ Daily Practice
+
All 31 prepositions · one-tap · keep them sharp
+ {dailySc &&
{dailySc.c}/{dailySc.t}
} +
+ {dailyPct !== null &&
=70?GREEN:RED}}>{dailyPct}%
} +
+ )} {tab === 'prep' && (
{ setLearnStep(0); setScr('learn'); }}> + onClick={()=>{ setLearnStep(0); setScr('learn'); }}>
📚 Lernen
Chunk the lists first — Akkusativ, Dativ, Wechsel, Genitiv
)} - {LEVELS.filter(l => l.tab === tab).sort((a,b)=>a.id-b.id).map(l => { - const u = unlocked(l.id); - const s = st.sc[l.id]; - const p = s ? Math.round(s.c/s.t*100) : null; - return ( -
startLv(l.id)}> -
-
{l.name}
-
{l.desc}
+ {LEVELS.filter(l => l.tab === tab && !l.daily).sort((a,b)=>a.id-b.id).map(l => { + const u = unlocked(l.id); + const s = st.sc[l.id]; + const p = s ? Math.round(s.c/s.t*100) : null; + return ( +
startLv(l.id)}> +
+
{l.name}
+
{l.desc}
{s &&
{s.c}/{s.t}
}
{p !== null &&
=70?GREEN:RED}}>{p}%
} @@ -437,11 +477,11 @@ export default function App() {
))} -
- )} -
+
+ )} +
-
+
{learnStep > 0 && ( @@ -467,7 +507,7 @@ export default function App() { color: rev?(ok?GREEN:RED):'#1a1a1a', minWidth: q.type==='poss' ? 110 : 80, }}> - {rev ? q.ans : (inp || '\u00A0\u00A0\u00A0\u00A0\u00A0')} + {rev ? q.ans : (inp || '     ')} )} @@ -652,8 +692,8 @@ export default function App() { )} {/* SUMMARY */} - {scr === 'summary' && ( -
+ {scr === 'summary' && ( +
Complete
{rc}/{ROUND} @@ -677,11 +717,11 @@ export default function App() {
-
- )} +
+ )} - {scr !== 'practice' && } -
+ {scr !== 'practice' && } +
{chart && setChart(false)}/>}