feat: Premium-Redesign + Fortschritts-UI

- Zentrales Design-Token-System in index.css (Farben/Spacing/Radien/Schatten/Fonts)
- Alle Live-Screens auf Tokens: Karten, BottomNav (aktiver Pill), Feed, Auth
- Auth: DM Sans entfernt, Akzent vereinheitlicht (Braun), Tokens gescoped
- Profil neu: Tagesziel-Ring, Streak-Heatmap, Wochen-Graph, echter Skills-Radar, Eckdaten
- Feed-EP-Badge mit Tagesziel-Ring (ProgressRing-Komponente)
- Game/Pro als gestaltetes 'Bald verfügbar' (ComingSoon)
- Konsumiert neue API: getStats/setDailyGoal, degradiert sauber bei 404

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-13 16:41:09 +02:00
parent 8154f08e04
commit 712f9a243c
16 changed files with 744 additions and 341 deletions

View File

@@ -1,7 +1,8 @@
import { useEffect, useState } from 'react'
import './Feed.css'
import { useAuth } from '../context/AuthContext'
import { getFeedPairs, saveProgress, getUserProgress } from '../api/directus'
import { getFeedPairs, saveProgress, getUserProgress, getStats } from '../api/directus'
import ProgressRing from '../components/ProgressRing'
import PairSentenceCard from '../components/PairSentenceCard'
import PairYesNoCard from '../components/PairYesNoCard'
import PairWordCard from '../components/PairWordCard'
@@ -24,6 +25,7 @@ export default function Feed() {
const [loading, setLoading] = useState(true)
const [empty, setEmpty] = useState(false)
const [totalEp, setTotalEp] = useState(null)
const [daily, setDaily] = useState(null) // { ep, daily_goal_ep } wenn /auth/stats verfügbar
// Target language from user profile, fall back to 'de'
const lang = user?.language_target_short || 'de'
@@ -43,37 +45,42 @@ export default function Feed() {
getUserProgress(token)
.then(p => setTotalEp(p.total_ep))
.catch(() => {})
// Tagesziel-Fortschritt degradiert lautlos, falls /auth/stats noch nicht deployed ist
getStats(token)
.then(s => { if (s?.today) setDaily(s.today) })
.catch(() => {})
}, [token])
function handleComplete(item, result) {
setDone(prev => new Set([...prev, item.meta.pairId]))
const correct = result === 'correct'
const earned = correct ? item.meta.points : 0
saveProgress({
pairId: item.meta.pairId,
correct,
points: correct ? item.meta.points : 0,
points: earned,
userToken: token,
})
.then(res => { if (res?.total_ep != null) setTotalEp(res.total_ep) })
.catch(err => console.error('saveProgress error', err))
// Tagesziel optimistisch hochzählen
if (earned > 0) setDaily(d => d ? { ...d, ep: (d.ep || 0) + earned } : d)
}
const visible = cards.filter(c => !done.has(c.meta.pairId))
if (loading) {
return (
<div className="feed">
<div style={{ padding: '60px 24px', textAlign: 'center', color: '#9A8F85', fontFamily: 'DM Sans, sans-serif' }}>
Lade Karten
</div>
<div className="feed page-enter">
<div className="feed-empty">Lade Karten</div>
</div>
)
}
if (empty || visible.length === 0) {
return (
<div className="feed">
<div style={{ padding: '60px 24px', textAlign: 'center', color: '#9A8F85', fontFamily: 'DM Sans, sans-serif' }}>
<div className="feed page-enter">
<div className="feed-empty">
{cards.length === 0
? 'Noch keine Inhalte verfügbar.'
: 'Super! Alle Karten abgeschlossen. 🎉'}
@@ -82,16 +89,22 @@ export default function Feed() {
)
}
const goalPct = daily && daily.daily_goal_ep ? (daily.ep || 0) / daily.daily_goal_ep : 0
return (
<div className="feed">
<div className="feed page-enter">
{totalEp != null && (
<div style={{
position: 'sticky', top: 8, zIndex: 5, alignSelf: 'center',
background: '#fff', border: '1px solid #EFE7DE', borderRadius: 999,
padding: '6px 14px', margin: '4px auto 8px', fontFamily: 'DM Sans, sans-serif',
fontWeight: 600, color: '#7A6A58', boxShadow: '0 2px 8px rgba(0,0,0,0.05)',
}}>
{totalEp} EP
<div className="ep-badge">
<ProgressRing
value={daily ? goalPct : 1}
size={26} stroke={4}
color={goalPct >= 1 ? 'var(--success)' : 'var(--gold)'}
>
<span style={{ fontSize: 11 }}>{goalPct >= 1 ? '✓' : '⭐'}</span>
</ProgressRing>
<span className="ep-value">
{totalEp}<small>EP{daily ? ` · ${daily.ep || 0}/${daily.daily_goal_ep} heute` : ''}</small>
</span>
</div>
)}
{visible.map((item) => {