+
diff --git a/src/BottomNav.css b/src/BottomNav.css
index f46db1b..e7b260e 100644
--- a/src/BottomNav.css
+++ b/src/BottomNav.css
@@ -1,8 +1,10 @@
.bottom-nav {
display: flex;
- background: #C4A882;
- border-top: 1px solid #D4B896;
- padding-bottom: env(safe-area-inset-bottom);
+ background: var(--surface);
+ border-top: 1px solid var(--border-soft);
+ padding: 6px 8px;
+ padding-bottom: calc(6px + env(safe-area-inset-bottom));
+ box-shadow: 0 -2px 16px rgba(60, 40, 20, 0.05);
}
.nav-item {
@@ -11,34 +13,45 @@
flex-direction: column;
align-items: center;
justify-content: center;
- padding: 10px 0;
+ padding: 8px 0 6px;
border: none;
background: none;
cursor: pointer;
- color: rgba(74, 55, 40, 0.45);
- transition: color 0.2s;
- gap: 3px;
-}
-
-.nav-item.active {
- color: #7A5C3A;
+ color: var(--text-soft);
+ transition: color var(--dur-fast) var(--ease);
+ gap: 4px;
}
.nav-icon {
- width: 24px;
- height: 24px;
+ width: 44px;
+ height: 30px;
display: flex;
align-items: center;
justify-content: center;
+ border-radius: var(--r-pill);
+ transition: background var(--dur) var(--ease), transform var(--dur) var(--ease);
}
.nav-icon svg {
- width: 24px;
- height: 24px;
+ width: 23px;
+ height: 23px;
+}
+
+.nav-item.active {
+ color: var(--accent);
+}
+
+.nav-item.active .nav-icon {
+ background: var(--accent-soft);
+}
+
+.nav-item:active .nav-icon {
+ transform: scale(0.9);
}
.nav-label {
- font-family: 'Nunito', sans-serif;
+ font-family: var(--font-ui);
font-size: 11px;
font-weight: 700;
+ letter-spacing: 0.02em;
}
diff --git a/src/api/directus.js b/src/api/directus.js
index a4331c5..a8689e2 100644
--- a/src/api/directus.js
+++ b/src/api/directus.js
@@ -110,6 +110,25 @@ export async function getUserProgress(userToken) {
return { total_ep: me.total_ep || 0, streak_days: me.streak_days || 0, level: me.level || 0 }
}
+// Detaillierte Statistik fürs Profil: { daily[], today, totals, skills }.
+export async function getStats(userToken) {
+ const res = await fetch(`${BASE}/auth/stats`, { headers: auth(userToken) })
+ const data = await res.json().catch(() => ({}))
+ if (!res.ok) throw new Error(data.error || 'Statistik konnte nicht geladen werden.')
+ return data
+}
+
+// Tagesziel (EP/Tag) setzen. Gibt { daily_goal_ep } zurück.
+export async function setDailyGoal(dailyGoalEp, userToken) {
+ const res = await fetch(`${BASE}/auth/goal`, {
+ method: 'PUT', headers: auth(userToken),
+ body: JSON.stringify({ daily_goal_ep: dailyGoalEp }),
+ })
+ const data = await res.json().catch(() => ({}))
+ if (!res.ok) throw new Error(data.error || 'Tagesziel konnte nicht gespeichert werden.')
+ return data
+}
+
// ── Stubs (content-Endpunkte kommen später) ───────────────────────────────────
export async function getActiveLearningPair() { return null }
diff --git a/src/components/ComingSoon.css b/src/components/ComingSoon.css
new file mode 100644
index 0000000..c31d85b
--- /dev/null
+++ b/src/components/ComingSoon.css
@@ -0,0 +1,82 @@
+.coming-soon {
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--bg);
+ padding: var(--sp-5);
+ overflow-y: auto;
+}
+
+.cs-card {
+ width: 100%;
+ max-width: 340px;
+ background: var(--surface);
+ border: 1px solid var(--border-soft);
+ border-radius: var(--r-lg);
+ box-shadow: var(--shadow-card);
+ padding: var(--sp-6) var(--sp-5);
+ text-align: center;
+}
+
+.cs-icon {
+ width: 68px;
+ height: 68px;
+ margin: 0 auto var(--sp-4);
+ border-radius: 50%;
+ background: var(--accent-soft);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--accent);
+}
+.cs-icon svg { width: 32px; height: 32px; }
+
+.cs-badge {
+ display: inline-block;
+ font-size: 10px;
+ font-weight: 800;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+ color: var(--gold);
+ background: var(--gold-soft);
+ border-radius: var(--r-pill);
+ padding: 4px 12px;
+ margin-bottom: var(--sp-3);
+}
+
+.cs-title {
+ font-family: var(--font-display);
+ font-size: 24px;
+ font-weight: 700;
+ color: var(--text-strong);
+ margin-bottom: var(--sp-2);
+}
+
+.cs-subtitle {
+ font-size: 14px;
+ color: var(--text-muted);
+ line-height: 1.6;
+ margin-bottom: var(--sp-4);
+}
+
+.cs-teaser {
+ list-style: none;
+ text-align: left;
+ display: inline-flex;
+ flex-direction: column;
+ gap: var(--sp-2);
+}
+.cs-teaser li {
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--text);
+ padding-left: 24px;
+ position: relative;
+}
+.cs-teaser li::before {
+ content: '✦';
+ position: absolute;
+ left: 4px;
+ color: var(--gold);
+}
diff --git a/src/components/ComingSoon.jsx b/src/components/ComingSoon.jsx
new file mode 100644
index 0000000..5422fb5
--- /dev/null
+++ b/src/components/ComingSoon.jsx
@@ -0,0 +1,20 @@
+import './ComingSoon.css'
+
+// Gestaltetes "Bald verfügbar" für noch nicht gebaute Bereiche.
+export default function ComingSoon({ icon, title, subtitle, teaser }) {
+ return (
+
+
+
{icon}
+
Bald verfügbar
+
{title}
+
{subtitle}
+ {teaser && (
+
+ {teaser.map((t, i) => - {t}
)}
+
+ )}
+
+
+ )
+}
diff --git a/src/components/PairCards.css b/src/components/PairCards.css
index 5627b8f..81142ab 100644
--- a/src/components/PairCards.css
+++ b/src/components/PairCards.css
@@ -6,13 +6,10 @@
.pair-card {
width: 100%;
max-width: 380px;
- border-radius: 22px;
+ border-radius: var(--r-lg);
overflow: hidden;
- background: #FDFAF6;
- box-shadow:
- 0 1px 2px rgba(60, 40, 20, 0.06),
- 0 4px 16px rgba(60, 40, 20, 0.09),
- 0 12px 40px rgba(60, 40, 20, 0.06);
+ background: var(--surface);
+ box-shadow: var(--shadow-card);
}
/* ── Header (sits below image) ── */
@@ -27,14 +24,14 @@
font-weight: 500;
letter-spacing: 0.09em;
color: #6B6556;
- font-family: 'DM Sans', 'Nunito', sans-serif;
+ font-family: var(--font-ui);
text-transform: uppercase;
}
.pair-points-pill {
- color: #C4A85A;
+ color: var(--gold);
font-size: 12px;
font-weight: 500;
- font-family: 'DM Sans', 'Nunito', sans-serif;
+ font-family: var(--font-ui);
display: flex;
align-items: center;
gap: 4px;
@@ -54,7 +51,7 @@
letter-spacing: 0.10em;
text-transform: uppercase;
color: #A89F8C;
- font-family: 'DM Sans', 'Nunito', sans-serif;
+ font-family: var(--font-ui);
margin-bottom: 8px;
}
@@ -154,7 +151,7 @@
}
.pair-chip-highlight-target {
display: block;
- font-family: 'Lora', Georgia, serif;
+ font-family: var(--font-display);
font-size: 24px;
font-weight: 700;
color: #3A2515;
@@ -163,7 +160,7 @@
}
.pair-chip-highlight-native {
display: block;
- font-family: 'Nunito', sans-serif;
+ font-family: var(--font-ui);
font-size: 13px;
color: #9A7D60;
font-weight: 600;
@@ -198,7 +195,7 @@
background: #F0EDE3;
border-radius: 30px;
border: 0.5px solid #D8D3C5;
- font-family: 'Lora', Georgia, serif;
+ font-family: var(--font-display);
font-size: 13px;
font-style: italic;
color: #7A5C2E;
@@ -252,7 +249,7 @@
/* Sentence text (text-type cards) */
.pair-sentence {
- font-family: 'Lora', Georgia, serif;
+ font-family: var(--font-display);
font-size: 18px;
line-height: 1.7;
color: #3A2515;
@@ -261,7 +258,7 @@
/* Question text */
.pair-question {
- font-family: 'Lora', Georgia, serif;
+ font-family: var(--font-display);
font-size: 17px;
font-weight: 600;
line-height: 1.65;
@@ -271,7 +268,7 @@
/* Hint — native language translation */
.pair-hint {
- font-family: 'Nunito', sans-serif;
+ font-family: var(--font-ui);
font-size: 13px;
color: #A08868;
line-height: 1.5;
@@ -288,7 +285,7 @@
padding: 14px 10px;
border-radius: 13px;
border: none;
- font-family: 'Nunito', sans-serif;
+ font-family: var(--font-ui);
font-size: 15px;
font-weight: 700;
cursor: pointer;
@@ -300,7 +297,7 @@
.pair-btn:disabled { opacity: 0.75; cursor: default; }
.pair-btn-primary {
- background: #5C3D22;
+ background: var(--accent-strong);
color: #F5EDE0;
}
.pair-btn-locked {
@@ -309,15 +306,15 @@
cursor: not-allowed;
}
.pair-btn-yes {
- background: #3D7055;
+ background: var(--success);
color: #fff;
}
.pair-btn-no {
- background: #A84040;
+ background: var(--danger);
color: #fff;
}
-.pair-btn-correct { background: #3D7055; color: #fff; }
-.pair-btn-wrong { background: #A84040; color: #fff; }
+.pair-btn-correct { background: var(--success); color: #fff; }
+.pair-btn-wrong { background: var(--danger); color: #fff; }
/* ── Word option buttons ── */
.pair-options {
@@ -332,7 +329,7 @@
border: 1.5px solid #DDD0BF;
background: #FDFAF6;
color: #3A2515;
- font-family: 'Nunito', sans-serif;
+ font-family: var(--font-ui);
font-size: 15px;
font-weight: 600;
cursor: pointer;
@@ -344,17 +341,17 @@
}
.pair-option-btn:active:not(:disabled) { transform: scale(0.95); }
.pair-option-btn.selected { background: #FFF0DC; border-color: #C4A85A; color: #5C3D22; }
-.pair-option-btn.correct { background: #3D7055; color: #fff; border-color: #3D7055; }
-.pair-option-btn.wrong { background: #A84040; color: #fff; border-color: #A84040; }
+.pair-option-btn.correct { background: var(--success); color: #fff; border-color: var(--success); }
+.pair-option-btn.wrong { background: var(--danger); color: #fff; border-color: var(--danger); }
.pair-option-btn:disabled { cursor: default; }
/* ── Feedback ── */
.pair-feedback {
- font-family: 'Nunito', sans-serif;
+ font-family: var(--font-ui);
font-size: 14px;
font-weight: 700;
padding: 12px 0 2px;
color: #3A2515;
}
-.pair-feedback.correct { color: #3D7055; }
-.pair-feedback.wrong { color: #A84040; }
+.pair-feedback.correct { color: var(--success); }
+.pair-feedback.wrong { color: var(--danger); }
diff --git a/src/components/ProgressRing.jsx b/src/components/ProgressRing.jsx
new file mode 100644
index 0000000..37f670f
--- /dev/null
+++ b/src/components/ProgressRing.jsx
@@ -0,0 +1,35 @@
+// Ruhiger SVG-Donut für Tagesziel-Fortschritt. Wiederverwendet in Feed (klein) und Profil (groß).
+export default function ProgressRing({
+ value = 0, // 0..1
+ size = 26,
+ stroke = 4,
+ track = 'var(--surface-2)',
+ color = 'var(--gold)',
+ children,
+}) {
+ const r = (size - stroke) / 2
+ const c = 2 * Math.PI * r
+ const pct = Math.max(0, Math.min(1, value))
+ const dash = c * pct
+ return (
+
+
+ {children != null && (
+
+ {children}
+
+ )}
+
+ )
+}
diff --git a/src/components/auth/AuthScreen.jsx b/src/components/auth/AuthScreen.jsx
index b381a73..dd0da60 100644
--- a/src/components/auth/AuthScreen.jsx
+++ b/src/components/auth/AuthScreen.jsx
@@ -3,14 +3,15 @@ import LoginForm from './LoginForm'
import RegisterStep1 from './RegisterStep1'
import RegisterStep2 from './RegisterStep2'
+// Legacy-Variablennamen der Auth-Formulare auf die globalen Design-Tokens mappen.
+// Auf .auth-root gescoped, damit nichts ins :root der App leakt.
const css = `
- :root {
- --bg: #F5F0E8; --surface: #FFFCF7; --border: #E2DAD0;
- --text: #2C2520; --muted: #9A8F85; --accent: #5C7A5E;
- --accent-lt: #EAF0EA; --danger: #C0544A; --danger-lt: #FBF0EF;
- --radius: 14px;
+ .auth-root {
+ --muted: var(--text-muted);
+ --accent-lt: var(--accent-soft);
+ --danger-lt: var(--danger-soft);
+ --radius: var(--r-sm);
}
- @import url('https://fonts.googleapis.com/css2?family=Lora:wght@400;500&family=DM+Sans:wght@300;400;500&display=swap');
@keyframes fadeUp { from { opacity:0; transform:translateY(8px); } to { opacity:1; transform:none; } }
`
@@ -32,7 +33,7 @@ export default function AuthScreen() {
return (
<>
-
+
{/* Brand */}
@@ -42,7 +43,7 @@ export default function AuthScreen() {
-
HejYou
+
HejYou
Sprachen lernen wie ein Kind
@@ -53,7 +54,7 @@ export default function AuthScreen() {
-
+
Willkommen, {successName}!
Dein Abenteuer beginnt jetzt.
diff --git a/src/components/auth/auth.module.css b/src/components/auth/auth.module.css
index 8c75324..ab57f76 100644
--- a/src/components/auth/auth.module.css
+++ b/src/components/auth/auth.module.css
@@ -16,7 +16,7 @@
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--bg);
- font-family: 'DM Sans', sans-serif;
+ font-family: var(--font-ui);
font-size: 15px;
color: var(--text);
outline: none;
@@ -26,7 +26,7 @@
}
.input:focus {
border-color: var(--accent);
- box-shadow: 0 0 0 3px rgba(92,122,94,0.12);
+ box-shadow: 0 0 0 3px rgba(122, 92, 58, 0.14);
background: var(--surface);
}
@@ -46,12 +46,12 @@
width: 100%; padding: 13px; margin-top: 8px;
background: var(--accent); color: #fff;
border: none; border-radius: var(--radius);
- font-family: 'DM Sans', sans-serif;
+ font-family: var(--font-ui);
font-size: 15px; font-weight: 500; cursor: pointer;
display: flex; align-items: center; justify-content: center; gap: 8px;
transition: background 0.2s, transform 0.1s;
}
-.btn:hover:not(:disabled) { background: #4a6650; }
+.btn:hover:not(:disabled) { background: var(--accent-strong); }
.btn:active:not(:disabled) { transform: scale(0.98); }
.btn:disabled { background: var(--border); color: var(--muted); cursor: not-allowed; }
diff --git a/src/index.css b/src/index.css
index 92c5585..82af39b 100644
--- a/src/index.css
+++ b/src/index.css
@@ -1,4 +1,65 @@
-@import url('https://fonts.googleapis.com/css2?family=Lora:wght@700&family=Nunito:wght@400;500;600;700;800&display=swap');
+@import url('https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400;0,500;0,600;0,700;1,500&family=Nunito:wght@400;500;600;700;800&display=swap');
+
+/* ── Design Tokens ──────────────────────────────────────────────
+ Single source of truth. Warmes, ruhiges Konzept – nur diszipliniert.
+ Bestehende Palette normalisiert (ein Grün, ein Rot, klare Skalen). */
+:root {
+ /* Flächen & Hintergrund */
+ --bg: #EDE0CE; /* warmes Creme – App-Hintergrund */
+ --bg-2: #F2E8DA; /* leicht heller, für Sektionen */
+ --surface: #FBF7F0; /* Karten / Panels */
+ --surface-2: #F4EEE3; /* eingelassene Flächen, Tracks */
+ --surface-sunk: #ECE3D4; /* Vertiefungen / Heatmap-Leerzelle */
+
+ /* Text */
+ --text: #4A3728; /* primärer warmer Braunton */
+ --text-strong: #3A2515; /* Überschriften / Betonung */
+ --text-muted: #8C7A65; /* sekundär */
+ --text-soft: #A89F8C; /* tertiär / Hints */
+
+ /* Akzente */
+ --accent: #7A5C3A; /* Aktion / aktiv */
+ --accent-strong: #5C3D22;
+ --accent-soft: #EFE6D6; /* heller Akzent-Hintergrund (Pills) */
+ --gold: #C4A85A; /* EP / Belohnung */
+ --gold-soft: #F0E4C4;
+ --success: #3D7055; /* genau ein Grün */
+ --success-soft: #E4EDE5;
+ --danger: #C0544A; /* genau ein warmes Rot */
+ --danger-soft: #FBF0EF;
+
+ /* Linien */
+ --border: #DDD0BF;
+ --border-soft: #E7DDCD;
+
+ /* Spacing-Skala */
+ --sp-1: 4px;
+ --sp-2: 8px;
+ --sp-3: 12px;
+ --sp-4: 16px;
+ --sp-5: 24px;
+ --sp-6: 32px;
+
+ /* Radien */
+ --r-sm: 12px;
+ --r-md: 16px;
+ --r-lg: 22px;
+ --r-pill: 999px;
+
+ /* Schatten – warm getönt, nie hartes Schwarz */
+ --shadow-soft: 0 1px 2px rgba(60, 40, 20, 0.05), 0 2px 8px rgba(60, 40, 20, 0.06);
+ --shadow-card: 0 1px 2px rgba(60, 40, 20, 0.06), 0 4px 16px rgba(60, 40, 20, 0.09), 0 12px 40px rgba(60, 40, 20, 0.06);
+ --shadow-pop: 0 6px 24px rgba(60, 40, 20, 0.16);
+
+ /* Typografie */
+ --font-ui: 'Nunito', -apple-system, BlinkMacSystemFont, sans-serif;
+ --font-display: 'Lora', Georgia, serif;
+
+ /* Bewegung */
+ --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
+ --dur-fast: 0.15s;
+ --dur: 0.28s;
+}
* {
margin: 0;
@@ -7,12 +68,14 @@
}
body {
- font-family: 'Nunito', -apple-system, BlinkMacSystemFont, sans-serif;
- background: #EDE0CE;
- color: #4A3728;
+ font-family: var(--font-ui);
+ background: var(--bg);
+ color: var(--text);
height: 100dvh;
overflow: hidden;
line-height: 1.7;
+ -webkit-font-smoothing: antialiased;
+ text-rendering: optimizeLegibility;
}
#root {
@@ -20,3 +83,30 @@ body {
display: flex;
flex-direction: column;
}
+
+/* Gemeinsamer, ruhiger Loader/Spinner */
+@keyframes spin { to { transform: rotate(360deg); } }
+
+.app-spinner {
+ width: 32px;
+ height: 32px;
+ border: 2px solid var(--border-soft);
+ border-top-color: var(--accent);
+ border-radius: 50%;
+ animation: spin 0.7s linear infinite;
+}
+
+/* Dezenter Seitenwechsel */
+@keyframes pageEnter {
+ from { opacity: 0; transform: translateY(6px); }
+ to { opacity: 1; transform: none; }
+}
+.page-enter { animation: pageEnter var(--dur) var(--ease); }
+
+@media (prefers-reduced-motion: reduce) {
+ *, *::before, *::after {
+ animation-duration: 0.001ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.001ms !important;
+ }
+}
diff --git a/src/pages/Feed.css b/src/pages/Feed.css
index b8cd936..7b72451 100644
--- a/src/pages/Feed.css
+++ b/src/pages/Feed.css
@@ -1,6 +1,6 @@
.feed {
height: 100%;
- background: #EDE0CE;
+ background: var(--bg);
overflow-y: auto;
scroll-snap-type: y mandatory;
-webkit-overflow-scrolling: touch;
@@ -12,8 +12,55 @@
display: flex;
align-items: flex-start;
justify-content: center;
- padding: 16px 20px;
- padding-bottom: 24px;
+ padding: var(--sp-4) var(--sp-5);
+ padding-bottom: var(--sp-5);
flex-shrink: 0;
box-sizing: border-box;
}
+
+.feed-empty {
+ padding: 60px var(--sp-5);
+ text-align: center;
+ color: var(--text-muted);
+ font-family: var(--font-ui);
+ font-weight: 600;
+}
+
+/* EP-Badge + Tagesziel-Ring */
+.ep-badge {
+ position: sticky;
+ top: 10px;
+ z-index: 5;
+ align-self: center;
+ display: inline-flex;
+ align-items: center;
+ gap: var(--sp-2);
+ background: var(--surface);
+ border: 1px solid var(--border-soft);
+ border-radius: var(--r-pill);
+ padding: 5px 14px 5px 7px;
+ margin: 8px auto 10px;
+ font-family: var(--font-ui);
+ font-weight: 800;
+ color: var(--text-strong);
+ box-shadow: var(--shadow-soft);
+ animation: epPop var(--dur) var(--ease);
+}
+
+@keyframes epPop {
+ from { opacity: 0; transform: translateY(-6px); }
+ to { opacity: 1; transform: none; }
+}
+
+.ep-badge .ep-ring { display: block; flex-shrink: 0; }
+
+.ep-badge .ep-value {
+ font-size: 14px;
+ line-height: 1;
+}
+.ep-badge .ep-value small {
+ font-weight: 700;
+ color: var(--text-muted);
+ font-size: 12px;
+ margin-left: 2px;
+}
diff --git a/src/pages/Feed.jsx b/src/pages/Feed.jsx
index 460e2ed..3e88f14 100644
--- a/src/pages/Feed.jsx
+++ b/src/pages/Feed.jsx
@@ -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 (
-
-
- Lade Karten…
-
+
)
}
if (empty || visible.length === 0) {
return (
-
-
+
+
{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 (
-
+
{totalEp != null && (
-
- ⭐ {totalEp} EP
+
+
= 1 ? 'var(--success)' : 'var(--gold)'}
+ >
+ {goalPct >= 1 ? '✓' : '⭐'}
+
+
+ {totalEp}EP{daily ? ` · ${daily.ep || 0}/${daily.daily_goal_ep} heute` : ''}
+
)}
{visible.map((item) => {
diff --git a/src/pages/Game.jsx b/src/pages/Game.jsx
index fe8b55e..8aef380 100644
--- a/src/pages/Game.jsx
+++ b/src/pages/Game.jsx
@@ -1,7 +1,20 @@
+import ComingSoon from '../components/ComingSoon'
+
+const IconGame = () => (
+
+)
+
export default function Game() {
return (
-
-
Dieser Bereich wird später kommen.
-
+
}
+ title="Spielend lernen"
+ subtitle="Kurze, verspielte Übungsrunden, die dein Wissen auf die Probe stellen – entspannt und mit Belohnung."
+ teaser={['Tägliche Mini-Challenges', 'Wiederholung schwacher Wörter', 'Extra-EP für Bestzeiten']}
+ />
)
}
diff --git a/src/pages/Pro.jsx b/src/pages/Pro.jsx
index c92dcaf..6b4a239 100644
--- a/src/pages/Pro.jsx
+++ b/src/pages/Pro.jsx
@@ -1,7 +1,18 @@
+import ComingSoon from '../components/ComingSoon'
+
+const IconPro = () => (
+
+)
+
export default function Pro() {
return (
-
-
Dieser Bereich wird später kommen.
-
+
}
+ title="Snakkimo Pro"
+ subtitle="Lerne ohne Grenzen – mit unbegrenzten Karten, Offline-Modus und persönlichen Lernpfaden."
+ teaser={['Unbegrenzte tägliche Karten', 'Persönliche Lernpfade', 'Detaillierte Statistiken']}
+ />
)
}
diff --git a/src/pages/Profil.css b/src/pages/Profil.css
index 12521d5..2e967a7 100644
--- a/src/pages/Profil.css
+++ b/src/pages/Profil.css
@@ -1,194 +1,195 @@
/* ── Layout ────────────────────────────────────────────────── */
.profil {
min-height: 100%;
- background: #EDE0CE;
- padding: 0 16px 32px;
+ background: var(--bg);
+ padding: 0 var(--sp-4) var(--sp-6);
overflow-y: auto;
+ position: relative;
}
+.profil-logout {
+ position: absolute;
+ top: 20px;
+ right: 4px;
+ background: none;
+ border: none;
+ cursor: pointer;
+ padding: 6px;
+ border-radius: var(--r-sm);
+ color: var(--text-soft);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: color var(--dur-fast), background var(--dur-fast);
+}
+.profil-logout:hover { color: var(--danger); background: var(--danger-soft); }
+
/* ── Header ────────────────────────────────────────────────── */
.profil-header {
display: flex;
align-items: center;
- gap: 14px;
- padding: 20px 4px 16px;
+ gap: var(--sp-3);
+ padding: 20px 4px var(--sp-4);
}
-.avatar-wrap {
- position: relative;
- flex-shrink: 0;
-}
+.avatar-wrap { position: relative; flex-shrink: 0; }
-/* Animated ring */
.avatar-ring {
- width: 64px;
- height: 64px;
+ width: 64px; height: 64px;
border-radius: 50%;
- background: conic-gradient(from 0deg, #EDE0CE, #C4A882, #7A5C3A, #D4B896, #EDE0CE);
- animation: spin-ring 4s linear infinite;
+ background: conic-gradient(from 0deg, var(--bg), var(--gold), var(--accent), var(--border), var(--bg));
+ animation: spin-ring 6s linear infinite;
padding: 2px;
}
-
-@keyframes spin-ring {
- to { transform: rotate(360deg); }
-}
+@keyframes spin-ring { to { transform: rotate(360deg); } }
.avatar-inner {
- width: 100%;
- height: 100%;
+ width: 100%; height: 100%;
border-radius: 50%;
- background: #EDE0CE;
+ background: var(--bg);
padding: 2px;
- display: flex;
- align-items: center;
- justify-content: center;
+ display: flex; align-items: center; justify-content: center;
}
.avatar {
- width: 100%;
- height: 100%;
+ width: 100%; height: 100%;
border-radius: 50%;
- background: linear-gradient(135deg, #7A5C3A, #4A3728);
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 18px;
- font-weight: 800;
- color: #F5EFE6;
- letter-spacing: 1px;
+ background: linear-gradient(135deg, var(--accent), var(--text));
+ display: flex; align-items: center; justify-content: center;
+ font-size: 18px; font-weight: 800; color: #F5EFE6; letter-spacing: 1px;
}
-/* Online dot */
.online-dot {
- position: absolute;
- bottom: 2px;
- right: 2px;
- width: 11px;
- height: 11px;
- background: #7A5C3A;
+ position: absolute; bottom: 2px; right: 2px;
+ width: 11px; height: 11px;
+ background: var(--success);
border-radius: 50%;
- border: 2px solid #EDE0CE;
+ border: 2px solid var(--bg);
z-index: 2;
}
-
.online-dot::after {
content: '';
- position: absolute;
- inset: -4px;
+ position: absolute; inset: -4px;
border-radius: 50%;
- border: 2px solid #7A5C3A;
- animation: pulse-ring 1.8s ease-out infinite;
+ border: 2px solid var(--success);
+ animation: pulse-ring 2s ease-out infinite;
}
-
@keyframes pulse-ring {
- 0% { opacity: 0.7; transform: scale(0.8); }
+ 0% { opacity: 0.6; transform: scale(0.8); }
100% { opacity: 0; transform: scale(1.8); }
}
-/* Level badge on avatar */
.avatar-level-badge {
- position: absolute;
- top: -4px;
- right: -6px;
- z-index: 3;
+ position: absolute; top: -4px; right: -6px; z-index: 3;
filter: drop-shadow(0 1px 3px rgba(74, 55, 40, 0.3));
}
-.profil-info {
- display: flex;
- flex-direction: column;
- gap: 2px;
-}
-
-.profil-name {
- font-family: 'Lora', Georgia, serif;
- font-size: 18px;
- font-weight: 700;
- color: #4A3728;
-}
-
-.profil-handle {
- font-size: 12px;
- color: #7A5C3A;
-}
+.profil-info { display: flex; flex-direction: column; gap: 2px; }
+.profil-name { font-family: var(--font-display); font-size: 19px; font-weight: 700; color: var(--text); }
+.profil-handle { font-size: 12px; color: var(--accent); }
+.profil-streak { font-size: 12px; color: #C4853A; margin-top: 4px; font-weight: 700; }
/* ── Cards ──────────────────────────────────────────────────── */
-.progress-card,
-.skills-card {
- background: #F5EFE6;
- border: 0.5px solid #D4B896;
- border-radius: 16px;
- padding: 16px;
- margin-bottom: 12px;
+.card {
+ background: var(--surface);
+ border: 1px solid var(--border-soft);
+ border-radius: var(--r-md);
+ padding: var(--sp-4);
+ margin-bottom: var(--sp-3);
+ box-shadow: var(--shadow-soft);
}
.card-title {
font-size: 11px;
- font-weight: 500;
- color: #8C7A65;
- letter-spacing: 0.07em;
- margin-bottom: 12px;
+ font-weight: 800;
+ color: var(--text-muted);
+ letter-spacing: 0.09em;
+ margin-bottom: var(--sp-3);
}
+/* ── Tagesziel ── */
+.goal-card { display: flex; align-items: center; gap: var(--sp-4); }
+.goal-ring-label { font-size: 14px; font-weight: 800; color: var(--text-strong); }
+.goal-text { flex: 1; }
+.goal-text .card-title { margin-bottom: 4px; }
+.goal-value { font-family: var(--font-display); font-size: 22px; font-weight: 700; color: var(--text-strong); line-height: 1.2; }
+.goal-value small { font-family: var(--font-ui); font-size: 13px; font-weight: 700; color: var(--text-muted); }
+.goal-hint { font-size: 12px; color: var(--text-muted); margin-top: 4px; }
+
/* ── XP Section ─────────────────────────────────────────────── */
-.xp-row {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 8px;
-}
-
-.lang-label {
- font-size: 13px;
- font-weight: 500;
- color: #4A3728;
-}
-
-.xp-value {
- font-size: 13px;
- font-weight: 500;
- color: #7A5C3A;
-}
-
-.xp-bar {
- background: #D4B896;
- border-radius: 99px;
- height: 8px;
- width: 100%;
- margin-bottom: 8px;
- overflow: hidden;
-}
+.xp-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--sp-2); }
+.lang-label { font-size: 13px; font-weight: 700; color: var(--text); }
+.xp-value { font-size: 13px; font-weight: 700; color: var(--accent); }
+.xp-bar { background: var(--surface-2); border-radius: var(--r-pill); height: 9px; width: 100%; margin-bottom: var(--sp-2); overflow: hidden; }
.xp-fill {
- height: 100%;
- border-radius: 99px;
- background: #7A5C3A;
+ height: 100%; border-radius: var(--r-pill);
+ background: linear-gradient(90deg, var(--accent), var(--gold));
+ transition: width 0.7s var(--ease);
}
-/* Level row */
-.level-row {
- display: flex;
- justify-content: space-between;
- align-items: center;
-}
+.level-row { display: flex; justify-content: space-between; align-items: center; }
+.level-pill { background: var(--accent); color: var(--bg); font-size: 11px; font-weight: 800; padding: 3px 11px; border-radius: var(--r-pill); }
+.level-hint { font-size: 11px; color: var(--text-muted); }
-.level-pill {
- background: #7A5C3A;
- color: #EDE0CE;
- font-size: 11px;
- font-weight: 500;
- padding: 3px 10px;
- border-radius: 99px;
+/* ── Wochen-Graph ── */
+.weekbars { display: flex; align-items: flex-end; gap: var(--sp-2); height: 96px; }
+.weekbar-col { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 6px; height: 100%; }
+.weekbar-track { flex: 1; width: 100%; display: flex; align-items: flex-end; }
+.weekbar-fill {
+ width: 100%;
+ min-height: 4px;
+ border-radius: var(--r-sm) var(--r-sm) 4px 4px;
+ background: var(--gold);
+ transition: height 0.6s var(--ease);
}
+.weekbar-fill.empty { background: var(--surface-sunk); }
+.weekbar-fill.today { background: var(--accent); }
+.weekbar-label { font-size: 10px; font-weight: 700; color: var(--text-soft); }
+.weekbar-label.today { color: var(--accent); }
-.level-hint {
- font-size: 11px;
- color: #8C7A65;
+/* ── Heatmap ── */
+.heatmap { display: flex; gap: 4px; justify-content: space-between; }
+.heatmap-col { display: flex; flex-direction: column; gap: 4px; flex: 1; }
+.heatmap-cell {
+ display: block;
+ width: 100%;
+ aspect-ratio: 1 / 1;
+ border-radius: 3px;
+ background: var(--surface-sunk);
}
+.heatmap-cell.lvl-0 { background: var(--surface-sunk); }
+.heatmap-cell.lvl-1 { background: #E4D3B0; }
+.heatmap-cell.lvl-2 { background: #D4B36E; }
+.heatmap-cell.lvl-3 { background: var(--gold); }
+.heatmap-cell.lvl-4 { background: var(--accent); }
-/* ── Radar ───────────────────────────────────────────────────── */
-.radar-wrap {
- display: flex;
- justify-content: center;
- padding: 8px 0 4px;
+.heatmap-legend {
+ display: flex; align-items: center; gap: 4px;
+ margin-top: var(--sp-3);
+ font-size: 10px; color: var(--text-soft); font-weight: 600;
+}
+.heatmap-legend .heatmap-cell { width: 11px; height: 11px; }
+
+/* ── Eckdaten ── */
+.stat-grid { display: flex; gap: var(--sp-2); margin-bottom: var(--sp-3); }
+.stat-tile {
+ flex: 1;
+ background: var(--surface);
+ border: 1px solid var(--border-soft);
+ border-radius: var(--r-md);
+ padding: var(--sp-3) var(--sp-2);
+ display: flex; flex-direction: column; align-items: center; gap: 2px;
+ box-shadow: var(--shadow-soft);
+}
+.stat-num { font-family: var(--font-display); font-size: 22px; font-weight: 700; color: var(--text-strong); }
+.stat-cap { font-size: 10px; font-weight: 700; color: var(--text-muted); letter-spacing: 0.04em; text-align: center; }
+
+/* ── Radar ── */
+.radar-wrap { display: flex; justify-content: center; padding: var(--sp-2) 0 4px; }
+.skills-empty { font-size: 13px; color: var(--text-muted); text-align: center; padding: var(--sp-4) 0; }
+
+.tracking-hint {
+ font-size: 12px; color: var(--text-muted); text-align: center;
+ padding: var(--sp-2) var(--sp-4) var(--sp-4);
}
diff --git a/src/pages/Profil.jsx b/src/pages/Profil.jsx
index ad65f96..f8be9f7 100644
--- a/src/pages/Profil.jsx
+++ b/src/pages/Profil.jsx
@@ -1,21 +1,13 @@
import { useEffect, useState } from 'react'
import './Profil.css'
import { useAuth } from '../context/AuthContext'
-import { getProfilData, getActiveLearningPair, getLanguageOptions, langById } from '../api/directus'
+import { getProfilData, getStats, getLanguageOptions, langById } from '../api/directus'
+import ProgressRing from '../components/ProgressRing'
function LogoutButton() {
const { logout } = useAuth()
return (
-