From 712f9a243c973ab9f5b23cf70a9603d022a6252b Mon Sep 17 00:00:00 2001 From: admin Date: Sat, 13 Jun 2026 16:41:09 +0200 Subject: [PATCH] feat: Premium-Redesign + Fortschritts-UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/App.jsx | 9 +- src/BottomNav.css | 45 +++-- src/api/directus.js | 19 ++ src/components/ComingSoon.css | 82 ++++++++ src/components/ComingSoon.jsx | 20 ++ src/components/PairCards.css | 53 +++-- src/components/ProgressRing.jsx | 35 ++++ src/components/auth/AuthScreen.jsx | 21 +- src/components/auth/auth.module.css | 8 +- src/index.css | 98 ++++++++- src/pages/Feed.css | 53 ++++- src/pages/Feed.jsx | 45 +++-- src/pages/Game.jsx | 19 +- src/pages/Pro.jsx | 17 +- src/pages/Profil.css | 265 ++++++++++++------------- src/pages/Profil.jsx | 296 +++++++++++++++++----------- 16 files changed, 744 insertions(+), 341 deletions(-) create mode 100644 src/components/ComingSoon.css create mode 100644 src/components/ComingSoon.jsx create mode 100644 src/components/ProgressRing.jsx diff --git a/src/App.jsx b/src/App.jsx index 6891765..5c2e3f6 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -15,9 +15,8 @@ function AppContent() { if (loading) { return ( -
-
- +
+
) } @@ -29,8 +28,8 @@ function AppContent() { const PageComponent = PAGES[page] || Feed return ( -
-
+
+
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… -
+
+
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 ( -