feat: Fortschritt spürbar machen – Momente, Momentum & Storytelling

- +EP-Float am Button + hochzählendes EP-Badge (EpFloat, useCountUp)
- Level-/Streak-/Tagesziel-Overlay (MilestoneOverlay), getriggert aus der saveProgress-Response
- Combo-Zähler + variables Lob, ermutigendes Fehler-Feedback statt stillem Verschwinden
- Session-Summary mit Story-Zeilen statt End-Sackgasse
- Profil führt mit %-bis-Level + Capability-Satz; Kategorie-Stufen, Wochenvergleich, Sound-Toggle
- Level-Kurve gespiegelt (utils/leveling.js); Level deploy-unabhängig aus EP abgeleitet

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 21:43:56 +02:00
parent 9e8af27d51
commit 039d2cbbf4
14 changed files with 665 additions and 36 deletions

View File

@@ -0,0 +1,35 @@
import { useEffect } from 'react'
import confetti from 'canvas-confetti'
import './Moments.css'
const COLORS = ['#C4A85A', '#7A5C2E', '#3D7055', '#E8C9A8', '#fff']
function celebrate() {
const reduce = window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches
if (reduce) return
confetti({ particleCount: 140, spread: 90, origin: { y: 0.5 }, colors: COLORS, scalar: 1 })
setTimeout(() => confetti({ particleCount: 70, spread: 110, origin: { y: 0.45 }, colors: COLORS, scalar: 0.8 }), 220)
}
// Texte je Milestone-Art. value = Level-Nummer / Streak-Tage / Tagesziel-EP.
function content({ kind, value }) {
if (kind === 'level') return { cls: '', icon: '🏆', title: `Level ${value} erreicht!`, sub: 'Du wächst spürbar — weiter so.' }
if (kind === 'streak') return { cls: 'streak', icon: '🔥', title: `${value} Tage am Stück!`, sub: 'Dranbleiben zahlt sich aus.' }
if (kind === 'goal') return { cls: 'goal', icon: '🎯', title: 'Tagesziel erreicht!', sub: 'Stark — heute hast du dein Pensum geschafft.' }
return { cls: '', icon: '🎉', title: 'Geschafft!', sub: '' }
}
export default function MilestoneOverlay({ milestone, onClose }) {
useEffect(() => { celebrate() }, [milestone])
const { cls, icon, title, sub } = content(milestone)
return (
<div className="milestone-overlay" onClick={onClose} role="dialog" aria-label={title}>
<div className="milestone-card" onClick={e => e.stopPropagation()}>
<div className={`milestone-badge ${cls}`} aria-hidden="true">{icon}</div>
<h2 className="milestone-title">{title}</h2>
{sub && <p className="milestone-sub">{sub}</p>}
<button className="milestone-btn" onClick={onClose} autoFocus>Weiter</button>
</div>
</div>
)
}