feat: Streak-Reminder – In-App-Nudge + lokale Tages-Erinnerung
- streak.js: Zustand aus last_practice_at (safe/at_risk/broken/none) - Feed: Loss-Aversion-Bar "X-Tage-Serie – nur noch Y Std heute!" wenn die Serie heute abläuft - Profil-Streak-Zeile zeigt Status (heute gesichert ✓ / noch X Std) - Lokale Tages-Erinnerung via @capacitor/local-notifications (kein APNs nötig), geplant bei Login, abgebrochen sobald heute geübt – nativ; web no-op Hinweis: nativ erst nach 'npx cap sync ios' + Rebuild aktiv. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,8 @@ import SessionSummary from '../components/SessionSummary'
|
||||
import useCountUp from '../hooks/useCountUp'
|
||||
import { levelForEp } from '../utils/leveling'
|
||||
import { playCorrect, playMilestone } from '../utils/sound'
|
||||
import { streakState } from '../utils/streak'
|
||||
import { cancelStreakReminder } from '../utils/streakReminder'
|
||||
|
||||
// Points per answer_type
|
||||
const POINTS = { text: 2, yes_no: 2, word: 3, question: 3 }
|
||||
@@ -39,6 +41,8 @@ export default function Feed() {
|
||||
const [milestones, setMilestones] = useState([]) // Queue: Level-Up / Streak / Tagesziel
|
||||
const [topCat, setTopCat] = useState(null) // stärkste Kategorie für die Session-Summary
|
||||
const [reloadKey, setReloadKey] = useState(0) // erneutes Laden nach der Summary
|
||||
const [practiced, setPracticed] = useState(false) // heute in dieser Session geübt?
|
||||
const [streakDismissed, setStreakDismissed] = useState(false)
|
||||
|
||||
// Session-Zähler (lokal, für die Abschluss-Summary) + zuletzt bekannter Fortschritt,
|
||||
// um Level-Up/Streak-Up im saveProgress-Response zu erkennen.
|
||||
@@ -129,6 +133,9 @@ export default function Feed() {
|
||||
const correct = result === 'correct'
|
||||
const earned = correct ? item.meta.points : 0
|
||||
|
||||
// Heute geübt → Serie gesichert: Nudge weg + geplante Erinnerung abbrechen.
|
||||
if (!practiced) { setPracticed(true); cancelStreakReminder() }
|
||||
|
||||
// Session-Zähler + Combo + Sound
|
||||
session.current.cards += 1
|
||||
if (correct) {
|
||||
@@ -213,8 +220,23 @@ export default function Feed() {
|
||||
|
||||
const goalPct = daily && daily.daily_goal_ep ? (daily.ep || 0) / daily.daily_goal_ep : 0
|
||||
|
||||
// Loss-Aversion-Nudge: Serie läuft heute ab (oder ist gerissen) und heute noch nicht geübt.
|
||||
const streak = streakState(user?.last_practice_at, user?.streak_days || 0)
|
||||
const showStreakNudge = !practiced && !streakDismissed &&
|
||||
(streak.state === 'at_risk' || streak.state === 'broken')
|
||||
|
||||
return (
|
||||
<div className="feed page-enter">
|
||||
{showStreakNudge && (
|
||||
<div className="streak-nudge" role="status">
|
||||
<span>
|
||||
{streak.state === 'at_risk'
|
||||
? `🔥 ${streak.streakDays}-Tage-Serie — nur noch ${streak.hoursLeft} Std heute!`
|
||||
: '🌱 Starte heute deine Serie neu'}
|
||||
</span>
|
||||
<button className="streak-nudge-x" onClick={() => setStreakDismissed(true)} aria-label="Schließen">×</button>
|
||||
</div>
|
||||
)}
|
||||
{combo >= 3 && (
|
||||
<div key={combo} className="combo-pill" aria-hidden="true">🔥 {combo} in Folge</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user