- saveProgress/getUserProgress (POST /auth/progress, /auth/me) - Feed: onComplete bucht EP, EP-Zähler oben - Profil: echte total_ep/level/streak statt hartkodiert Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
112 lines
3.7 KiB
JavaScript
112 lines
3.7 KiB
JavaScript
import { useEffect, useState } from 'react'
|
|
import './Feed.css'
|
|
import { useAuth } from '../context/AuthContext'
|
|
import { getFeedPairs, saveProgress, getUserProgress } from '../api/directus'
|
|
import PairSentenceCard from '../components/PairSentenceCard'
|
|
import PairYesNoCard from '../components/PairYesNoCard'
|
|
import PairWordCard from '../components/PairWordCard'
|
|
|
|
// Points per answer_type
|
|
const POINTS = { text: 2, yes_no: 2, word: 3, question: 3 }
|
|
|
|
function buildCard(pair) {
|
|
return {
|
|
type: pair.answer_type,
|
|
meta: { pairId: pair.id, points: pair.difficulty_level || POINTS[pair.answer_type] || 2, cardType: pair.answer_type },
|
|
card: pair,
|
|
}
|
|
}
|
|
|
|
export default function Feed() {
|
|
const { user, token } = useAuth()
|
|
const [cards, setCards] = useState([])
|
|
const [done, setDone] = useState(new Set())
|
|
const [loading, setLoading] = useState(true)
|
|
const [empty, setEmpty] = useState(false)
|
|
const [totalEp, setTotalEp] = useState(null)
|
|
|
|
// Target language from user profile, fall back to 'de'
|
|
const lang = user?.language_target_short || 'de'
|
|
|
|
useEffect(() => {
|
|
getFeedPairs(token, lang, 20)
|
|
.then(pairs => {
|
|
const built = pairs.map(buildCard)
|
|
setCards(built)
|
|
setEmpty(built.length === 0)
|
|
})
|
|
.catch(err => { console.error('Feed load error', err); setEmpty(true) })
|
|
.finally(() => setLoading(false))
|
|
}, [token, lang])
|
|
|
|
useEffect(() => {
|
|
getUserProgress(token)
|
|
.then(p => setTotalEp(p.total_ep))
|
|
.catch(() => {})
|
|
}, [token])
|
|
|
|
function handleComplete(item, result) {
|
|
setDone(prev => new Set([...prev, item.meta.pairId]))
|
|
const correct = result === 'correct'
|
|
saveProgress({
|
|
pairId: item.meta.pairId,
|
|
correct,
|
|
points: correct ? item.meta.points : 0,
|
|
userToken: token,
|
|
})
|
|
.then(res => { if (res?.total_ep != null) setTotalEp(res.total_ep) })
|
|
.catch(err => console.error('saveProgress error', err))
|
|
}
|
|
|
|
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>
|
|
)
|
|
}
|
|
|
|
if (empty || visible.length === 0) {
|
|
return (
|
|
<div className="feed">
|
|
<div style={{ padding: '60px 24px', textAlign: 'center', color: '#9A8F85', fontFamily: 'DM Sans, sans-serif' }}>
|
|
{cards.length === 0
|
|
? 'Noch keine Inhalte verfügbar.'
|
|
: 'Super! Alle Karten abgeschlossen. 🎉'}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="feed">
|
|
{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>
|
|
)}
|
|
{visible.map((item) => {
|
|
const cardWithMeta = { ...item.card, meta: item.meta }
|
|
const handler = (result) => handleComplete(item, result)
|
|
return (
|
|
<div key={item.meta.pairId} className="feed-slot">
|
|
{item.type === 'text' && <PairSentenceCard card={cardWithMeta} onComplete={handler} />}
|
|
{item.type === 'yes_no' && <PairYesNoCard card={cardWithMeta} onComplete={handler} />}
|
|
{item.type === 'word' && <PairWordCard card={cardWithMeta} onComplete={handler} />}
|
|
{item.type === 'question'&& <PairWordCard card={cardWithMeta} onComplete={handler} />}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
)
|
|
}
|