feat: Erfolge im Feed-Overlay + Profil-Sektion

- getAchievements() im API-Client
- unlocked_achievements aus saveProgress als Overlay-Typ 'achievement'
- ERFOLGE-Sektion im Profil (freigeschaltet/gesperrt), degradiert lautlos ohne Endpoint

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 21:54:02 +02:00
parent 039d2cbbf4
commit 98543979db
5 changed files with 60 additions and 5 deletions

View File

@@ -1,7 +1,7 @@
import { useEffect, useState, useMemo } from 'react'
import './Profil.css'
import { useAuth } from '../context/AuthContext'
import { getProfilData, getStats, getLanguageOptions, langById } from '../api/directus'
import { getProfilData, getStats, getLanguageOptions, langById, getAchievements } from '../api/directus'
import ProgressRing from '../components/ProgressRing'
import { levelInfo } from '../utils/leveling'
import { categoryTier, capabilitySentence } from '../utils/praise'
@@ -146,6 +146,7 @@ export default function Profil() {
const [profil, setProfil] = useState(null)
const [stats, setStats] = useState(null)
const [langs, setLangs] = useState([])
const [achievements, setAchievements] = useState([])
useEffect(() => {
const t = setTimeout(() => setRadarReady(true), 120)
@@ -160,6 +161,8 @@ export default function Profil() {
} catch { /* Fallback unten */ }
// Stats getrennt degradiert lautlos, falls /auth/stats noch nicht deployed ist
try { setStats(await getStats(token)) } catch { /* kein Tracking verfügbar */ }
// Erfolge degradiert lautlos, falls /auth/achievements noch nicht deployed ist
try { setAchievements(await getAchievements(token)) } catch { /* keine Erfolge verfügbar */ }
}
load()
}, [token])
@@ -315,6 +318,21 @@ export default function Profil() {
</div>
)}
{/* ── Erfolge ── */}
{achievements.length > 0 && (
<div className="card">
<p className="card-title">ERFOLGE · {achievements.filter(a => a.unlocked).length}/{achievements.length}</p>
<div className="ach-grid">
{achievements.map(a => (
<div key={a.key} className={`ach-tile ${a.unlocked ? 'on' : 'off'}`} title={a.label}>
<span className="ach-icon">{a.unlocked ? a.icon : '🔒'}</span>
<span className="ach-label">{a.label}</span>
</div>
))}
</div>
</div>
)}
{/* ── Streak-Kalender ── */}
{stats && (
<div className="card">