From e7dbb9d0a73e4d9d753acb3ff3a3ebd013f5c4ca Mon Sep 17 00:00:00 2001 From: admin Date: Tue, 2 Jun 2026 21:30:11 +0200 Subject: [PATCH] feat: EP-Fortschritt speichern + echtes Profil MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/api/directus.js | 22 ++++++++++++++++++++-- src/pages/Feed.jsx | 32 +++++++++++++++++++++++++++++--- src/pages/Profil.jsx | 19 ++++++++++--------- 3 files changed, 59 insertions(+), 14 deletions(-) diff --git a/src/api/directus.js b/src/api/directus.js index a05e5f6..a4331c5 100644 --- a/src/api/directus.js +++ b/src/api/directus.js @@ -90,6 +90,26 @@ export async function getFeedPairs(userToken, lang = 'de', limit = 20) { return data } +// ── Fortschritt / EP ──────────────────────────────────────────────────────── + +// Verbucht eine gelöste Karte: aktualisiert EP, Streak und Pair-Statistik. +// Gibt { total_ep, streak_days, level } zurück. +export async function saveProgress({ pairId, correct, points, userToken }) { + const res = await fetch(`${BASE}/auth/progress`, { + method: 'POST', headers: auth(userToken), + body: JSON.stringify({ pair_id: pairId, correct: !!correct, points: points || 0 }), + }) + const data = await res.json().catch(() => ({})) + if (!res.ok) throw new Error(data.error || 'Fortschritt konnte nicht gespeichert werden.') + return data +} + +// Aktueller Gesamtfortschritt des Users (EP, Streak, Level) via /auth/me. +export async function getUserProgress(userToken) { + const me = await getMe(userToken) + return { total_ep: me.total_ep || 0, streak_days: me.streak_days || 0, level: me.level || 0 } +} + // ── Stubs (content-Endpunkte kommen später) ─────────────────────────────────── export async function getActiveLearningPair() { return null } @@ -97,8 +117,6 @@ export async function addPointsToPair() { return false } export async function getWords() { return [] } export async function getQuestions() { return [] } export async function getQAPairsAtLevel() { return [] } -export async function getUserProgress() { return [] } -export async function saveProgress() { return null } export function assetUrl(fileId) { return fileId || null } export async function getProfilData(userToken) { return getMe(userToken) } export async function addPointsToUser() { return true } diff --git a/src/pages/Feed.jsx b/src/pages/Feed.jsx index de12d06..460e2ed 100644 --- a/src/pages/Feed.jsx +++ b/src/pages/Feed.jsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react' import './Feed.css' import { useAuth } from '../context/AuthContext' -import { getFeedPairs } from '../api/directus' +import { getFeedPairs, saveProgress, getUserProgress } from '../api/directus' import PairSentenceCard from '../components/PairSentenceCard' import PairYesNoCard from '../components/PairYesNoCard' import PairWordCard from '../components/PairWordCard' @@ -23,6 +23,7 @@ export default function Feed() { 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' @@ -38,8 +39,23 @@ export default function Feed() { .finally(() => setLoading(false)) }, [token, lang]) - function handleComplete(item) { + 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)) @@ -68,9 +84,19 @@ export default function Feed() { return (
+ {totalEp != null && ( +
+ ⭐ {totalEp} EP +
+ )} {visible.map((item) => { const cardWithMeta = { ...item.card, meta: item.meta } - const handler = () => handleComplete(item) + const handler = (result) => handleComplete(item, result) return (
{item.type === 'text' && } diff --git a/src/pages/Profil.jsx b/src/pages/Profil.jsx index 9bccd4e..ad65f96 100644 --- a/src/pages/Profil.jsx +++ b/src/pages/Profil.jsx @@ -135,14 +135,15 @@ export default function Profil() { load() }, [token, user.username]) - const displayName = profil?.username?.username_public || user?.username || '…' + const displayName = profil?.username || user?.username || '…' const initials = displayName.slice(0, 2).toUpperCase() - const points = pair?.points ?? profil?.points_total ?? 0 - const level = pair?.current_level ?? 1 - const xpMax = level * 500 - const xpPct = Math.min((points / xpMax) * 100, 100) - const toLang = pair ? langById(pair.language_to, langs) : null - const langLabel = toLang ? `${toLang.flag} ${toLang.label}` : 'Zielsprache' + const points = profil?.total_ep ?? 0 + const level = profil?.level ?? Math.floor(points / 500) + const epIntoLevel = points - level * 500 // EP innerhalb des aktuellen Levels + const epPerLevel = 500 + const xpPct = Math.min((epIntoLevel / epPerLevel) * 100, 100) + const toLang = profil?.language_target_short ? langById(profil.language_target_id, langs) : null + const langLabel = toLang ? `${toLang.flag} ${toLang.label}` : (profil?.language_target_titel || 'Zielsprache') const streak = profil?.streak_days ?? 0 return ( @@ -193,7 +194,7 @@ export default function Profil() {
{langLabel} - {points.toLocaleString('de')} / {xpMax.toLocaleString('de')} XP + {points.toLocaleString('de')} EP gesamt
@@ -202,7 +203,7 @@ export default function Profil() {
Level {level} - {(xpMax - points).toLocaleString('de')} XP bis Level {level + 1} + {(epPerLevel - epIntoLevel).toLocaleString('de')} EP bis Level {level + 1}