diff --git a/frontend/src/app.css b/frontend/src/app.css index b9ce273..d7ca460 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -371,12 +371,20 @@ body { font-family: var(--font); background: var(--bg); color: var(--text1); -we margin-bottom: 16px; } -/* KPI: immer gleich breite Spalten — Mobile 2×2, Desktop 1×4 (kein „einzelne volle Zeile“) */ -.dashboard-stat-grid { +/* + * Dashboard-Raster (KPI, Nebeneinander-Kacheln): 2 / 4 Spalten. + * StatCard, DashboardTile: span via --tile-sm / --tile-lg (JS clamp). + */ +.dashboard-stat-grid, +.dashboard-tile-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; - margin-bottom: 16px; +} + +.dashboard-stat-grid--mobile-4col, +.dashboard-tile-grid--mobile-4col { + grid-template-columns: repeat(4, minmax(0, 1fr)); } .dashboard-stat-card { @@ -385,34 +393,94 @@ body { font-family: var(--font); background: var(--bg); color: var(--text1); -we padding: 12px 10px; border: 1px solid var(--border); transition: border-color 0.15s; +} + +.dashboard-stat-card, +.dashboard-tile { min-width: 0; box-sizing: border-box; + grid-column: span var(--tile-sm, 1); } -.dashboard-summary-row { +@media (min-width: 1024px) { + .dashboard-stat-card, + .dashboard-tile { + grid-column: span var(--tile-lg, 1); + } +} + +/* ── Dashboard-Abschnitte (Überschrift + Trennlinie) ─ */ +.dashboard-section { + margin-bottom: 22px; +} + +.dashboard-section:last-child { + margin-bottom: 0; +} + +.dashboard-section__header { display: flex; - gap: 8px; - margin-bottom: 16px; + align-items: flex-end; + justify-content: space-between; + gap: 12px; + flex-wrap: wrap; + padding-bottom: 10px; + margin-bottom: 12px; + border-bottom: 1px solid var(--border); } -.dashboard-summary-row > .card { +.dashboard-section__headline { flex: 1; min-width: 0; } +.dashboard-section__title { + font-size: 13px; + font-weight: 700; + color: var(--text3); + text-transform: uppercase; + letter-spacing: 0.05em; + margin: 0; +} + +.dashboard-section__description { + font-size: 12px; + color: var(--text3); + margin: 4px 0 0 0; + line-height: 1.35; + max-width: 640px; +} + +.dashboard-section__body { + display: flex; + flex-direction: column; + gap: 12px; +} + +.dashboard-section__actions { + flex-shrink: 0; +} + +.dashboard-pill-row { + display: flex; + gap: 6px; + flex-wrap: wrap; +} + +/* Ernährung/Aktivität: Raster wie KPI; Kacheln per DashboardTile steuerbar */ +.dashboard-summary-row.dashboard-tile-grid { + margin-bottom: 0; +} + +.dashboard-erholung-grid .dashboard-tile > .card, +.dashboard-summary-row .dashboard-tile > .card { + height: 100%; +} + @media (min-width: 1024px) { - .dashboard-stat-grid { + .dashboard-stat-grid, + .dashboard-tile-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; } - - .dashboard-summary-row { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 12px; - } - - .dashboard-summary-row > .card { - flex: unset; - } } diff --git a/frontend/src/components/DashboardSection.jsx b/frontend/src/components/DashboardSection.jsx new file mode 100644 index 0000000..0609301 --- /dev/null +++ b/frontend/src/components/DashboardSection.jsx @@ -0,0 +1,31 @@ +/** + * Abschnitt mit optionalem Kopf (Titel, Beschreibung, Aktionen) und unterem Rand zur optischen Trennung. + */ +export default function DashboardSection({ + title, + description, + headerRight, + children, + className = '', + bodyClassName = '' +}) { + const showHeader = title || description || headerRight + return ( +
+ {showHeader && ( +
+
+ {title ?

{title}

: null} + {description ? ( +

{description}

+ ) : null} +
+ {headerRight ? ( +
{headerRight}
+ ) : null} +
+ )} +
{children}
+
+ ) +} diff --git a/frontend/src/components/DashboardTile.jsx b/frontend/src/components/DashboardTile.jsx new file mode 100644 index 0000000..4e86ef2 --- /dev/null +++ b/frontend/src/components/DashboardTile.jsx @@ -0,0 +1,29 @@ +import { + clampTileSpan, + DASHBOARD_TILE_GRID_COLS +} from '../utils/dashboardLayout' + +/** + * Kachel im Raster `.dashboard-tile-grid` / `.dashboard-stat-grid`. + * Standard: volle Zeile (Mobile 2/2, Desktop 4/4). Anpassbar via spanMobile / spanDesktop. + */ +export default function DashboardTile({ + children, + spanMobile = DASHBOARD_TILE_GRID_COLS.mobile, + spanDesktop = DASHBOARD_TILE_GRID_COLS.desktop, + className = '' +}) { + const sm = clampTileSpan(spanMobile, DASHBOARD_TILE_GRID_COLS.mobile) + const lg = clampTileSpan(spanDesktop, DASHBOARD_TILE_GRID_COLS.desktop) + return ( +
+ {children} +
+ ) +} diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx index fd0e4fa..eb2ae66 100644 --- a/frontend/src/pages/Dashboard.jsx +++ b/frontend/src/pages/Dashboard.jsx @@ -1,6 +1,6 @@ -import { useState, useEffect, useRef } from 'react' +import { useState, useEffect } from 'react' import { useNavigate } from 'react-router-dom' -import { Check, ChevronRight, Brain } from 'lucide-react' +import { Check, Brain } from 'lucide-react' import { LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid @@ -13,10 +13,17 @@ import EmailVerificationBanner from '../components/EmailVerificationBanner' import TrainingTypeDistribution from '../components/TrainingTypeDistribution' import SleepWidget from '../components/SleepWidget' import RestDaysWidget from '../components/RestDaysWidget' -import { getInterpretation, getStatusColor, getStatusBg } from '../utils/interpret' import Markdown from '../utils/Markdown' import dayjs from 'dayjs' import 'dayjs/locale/de' +import DashboardSection from '../components/DashboardSection' +import DashboardTile from '../components/DashboardTile' +import { + clampTileSpan, + DASHBOARD_TILE_GRID_COLS, + dashboardStatGridClassName, + dashboardTileGridClassName +} from '../utils/dashboardLayout' dayjs.locale('de') // ── Helpers ─────────────────────────────────────────────────────────────────── @@ -144,14 +151,37 @@ function Pill({ label, value, status, sub }) { } // ── Stat Card ───────────────────────────────────────────────────────────────── -function StatCard({ icon, label, value, unit, delta, deltaGoodWhenNeg=false, sub, onClick, color }) { +/** + * KPI-Kachel im Dashboard-Raster (`dashboard-stat-grid` / `dashboard-tile-grid`). + * @param {number} [spanMobile=1] Spaltenbreite unter 1024px (max. = Raster-Spalten mobile) + * @param {number} [spanDesktop=1] Spaltenbreite ≥1024px (max. 4) + */ +function StatCard({ + icon, + label, + value, + unit, + delta, + deltaGoodWhenNeg = false, + sub, + onClick, + color, + spanMobile = 1, + spanDesktop = 1 +}) { const deltaColor = delta==null ? null : (deltaGoodWhenNeg ? delta<0 : delta>0) ? 'var(--accent)' : 'var(--warn)' + const sm = clampTileSpan(spanMobile, DASHBOARD_TILE_GRID_COLS.mobile) + const lg = clampTileSpan(spanDesktop, DASHBOARD_TILE_GRID_COLS.desktop) return (
onClick&&(e.currentTarget.style.borderColor='var(--accent)')} onMouseLeave={e=>onClick&&(e.currentTarget.style.borderColor='var(--border)')}>
{icon}
@@ -260,7 +290,6 @@ export default function Dashboard() { const runPipeline = async () => { setPipelineLoading(true); setPipelineError(null) try { - const pid = localStorage.getItem('mitai-jinkendo_active_profile')||'' await api.insightPipeline() await load() } catch(e) { @@ -268,12 +297,7 @@ export default function Dashboard() { } finally { setPipelineLoading(false) } } - useEffect(()=>{ - console.log('[Dashboard] Component mounted, loading data...') - load() - },[]) - - console.log('[Dashboard] Rendering, loading=', loading, 'activeProfile=', activeProfile?.name) + useEffect(()=>{ load() },[]) if (loading) return
@@ -317,7 +341,11 @@ export default function Dashboard() { const hasAnyData = latestW||latestCal||nutrition.length>0 - console.log('[Dashboard] hasAnyData=', hasAnyData, 'latestW=', !!latestW, 'latestCal=', !!latestCal, 'nutrition.length=', nutrition.length) + const showNutrSummary = !!(avgKcal || avgProtein) + const showActSummary = actKcal != null + const summaryBoth = showNutrSummary && showActSummary + const summarySpanM = summaryBoth ? 1 : 2 + const summarySpanD = summaryBoth ? 2 : 4 return (
@@ -349,46 +377,54 @@ export default function Dashboard() { )} {hasAnyData && <> - {/* Quick weight entry */} -
-
-
⚖️ Gewicht heute
- + } + > +
+
- -
+ - {/* Key metrics — Mobile: flex-wrap; Desktop: 4-spaltig (RESPONSIVE_UI P3) */} -
- nav('/history')} color="#378ADD"/> - {latestCal?.body_fat_pct && nav('/history',{state:{tab:'body'}})} color={bfCat?.color}/>} - {latestCal?.lean_mass && nav('/history',{state:{tab:'body'}})}/>} - {avgKcal && nav('/history',{state:{tab:'nutrition'}})} color="#EF9F27"/>} -
- - {/* Status pills */} - {pills.length > 0 && ( -
- {pills.map((p,i)=>)} + +
+ nav('/history')} color="#378ADD"/> + {latestCal?.body_fat_pct && nav('/history',{state:{tab:'body'}})} color={bfCat?.color}/>} + {latestCal?.lean_mass && nav('/history',{state:{tab:'body'}})}/>} + {avgKcal && nav('/history',{state:{tab:'nutrition'}})} color="#EF9F27"/>}
- )} + {pills.length > 0 && ( +
+ {pills.map((p,i)=>)} +
+ )} +
- {/* Goals progress */} {(activeProfile?.goal_weight||activeProfile?.goal_bf_pct) && latestW && ( -
-
🎯 Ziele
+ +
{activeProfile?.goal_weight && latestW && (()=>{ const start = Math.max(...weights.map(w=>w.weight)) const curr = latestW.weight @@ -429,134 +465,167 @@ export default function Dashboard() { ) })()}
+
)} - {/* Combined chart */} {(weights.length>2||nutrition.length>2) && ( -
-
-
📊 Kalorien + Gewicht (30 Tage)
- -
- -
- Ø Kalorien - Gewicht -
-
+ } + > + +
+ +
+ Ø Kalorien + Gewicht +
+
+
+ )} - {/* Activity + Nutrition summary row */} -
- {(avgKcal||avgProtein) && ( -
nav('/history',{state:{tab:'nutrition'}})}> -
🍽️ ERNÄHRUNG (Ø 7T)
- {avgKcal &&
{avgKcal} kcal
} - {avgProtein &&
- {avgProtein}g Protein {proteinOk?'✓':'⚠️'} -
} -
→ Verlauf Ernährung
+ {(showNutrSummary || showActSummary) && ( + +
+ {showNutrSummary && ( + +
nav('/history',{state:{tab:'nutrition'}})}> +
🍽️ ERNÄHRUNG (Ø 7T)
+ {avgKcal &&
{avgKcal} kcal
} + {avgProtein &&
+ {avgProtein}g Protein {proteinOk?'✓':'⚠️'} +
} +
→ Verlauf Ernährung
+
+
+ )} + {showActSummary && ( + +
nav('/history',{state:{tab:'activity'}})}> +
🏋️ AKTIVITÄT (7T)
+
{actKcal} kcal
+
{recentAct.length} Trainings
+
→ Verlauf Aktivität
+
+
+ )}
- )} - {actKcal!=null && ( -
nav('/history',{state:{tab:'activity'}})}> -
🏋️ AKTIVITÄT (7T)
-
{actKcal} kcal
-
{recentAct.length} Trainings
-
→ Verlauf Aktivität
-
- )} -
+ + )} - {/* Sleep Widget */} -
- -
+ +
+ + + + + + +
+
- {/* Rest Days Widget */} -
- -
- - {/* Training Type Distribution */} {activities.length > 0 && ( -
-
-
🏋️ Trainingstyp-Verteilung
- -
- -
+ } + > + +
+ +
+
+ )} - {/* Goals Preview */} -
nav('/goals')}> -
-
🎯 Ziele
- -
-
- Definiere deine Trainingsmodus und konkrete Ziele für bessere KI-Analysen -
-
+ } + > + +
nav('/goals')}> +
+ Definiere deine Trainingsmodus und konkrete Ziele für bessere KI-Analysen +
+
+
+ - {/* Latest AI insight */} -
-
-
🤖 KI-Auswertung
- -
- {/* Pipeline trigger */} - - {pipelineError &&
{pipelineError}
} + } + > + +
+ + {pipelineError &&
{pipelineError}
} - {latestInsight ? ( - <> -
- Letzte Analyse: {dayjs(latestInsight.created).format('DD. MMMM YYYY, HH:mm')} -
-
- - {!showInsight && ( -
- )} -
- - - ) : ( -
- Noch keine KI-Auswertung vorhanden. - + {latestInsight ? ( + <> +
+ Letzte Analyse: {dayjs(latestInsight.created).format('DD. MMMM YYYY, HH:mm')} +
+
+ + {!showInsight && ( +
+ )} +
+ + + ) : ( +
+ Noch keine KI-Auswertung vorhanden. + +
+ )}
- )} -
+ + }
) diff --git a/frontend/src/utils/dashboardLayout.js b/frontend/src/utils/dashboardLayout.js new file mode 100644 index 0000000..9611c37 --- /dev/null +++ b/frontend/src/utils/dashboardLayout.js @@ -0,0 +1,26 @@ +/** + * Gemeinsames Raster für Dashboard-Kacheln (Mobile 2 / Desktop 4 Spalten). + * Optional Mobile 4: mobile auf 4 setzen + Klasse dashboard-tile-grid--mobile-4col. + */ +export const DASHBOARD_TILE_GRID_COLS = { mobile: 2, desktop: 4 } + +/** @param {number} span @param {number} maxCols */ +export function clampTileSpan(span, maxCols) { + const n = Number(span) + if (!Number.isFinite(n)) return 1 + return Math.min(maxCols, Math.max(1, Math.round(n))) +} + +/** @param {number} [mobileCols] 2 oder 4 */ +export function dashboardTileGridClassName(mobileCols = DASHBOARD_TILE_GRID_COLS.mobile) { + let c = 'dashboard-tile-grid' + if (mobileCols === 4) c += ' dashboard-tile-grid--mobile-4col' + return c +} + +/** KPI-Raster: dieselben Regeln wie `dashboard-tile-grid`, plus Legacy-Klasse `dashboard-stat-grid`. */ +export function dashboardStatGridClassName(mobileCols = DASHBOARD_TILE_GRID_COLS.mobile) { + let c = 'dashboard-stat-grid dashboard-tile-grid' + if (mobileCols === 4) c += ' dashboard-stat-grid--mobile-4col dashboard-tile-grid--mobile-4col' + return c +}