feat: Refactor dashboard layout with new DashboardSection and DashboardTile components for improved responsiveness and organization
This commit is contained in:
parent
d51bfd3daa
commit
422a117026
|
|
@ -371,12 +371,20 @@ body { font-family: var(--font); background: var(--bg); color: var(--text1); -we
|
||||||
margin-bottom: 16px;
|
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;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
gap: 8px;
|
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 {
|
.dashboard-stat-card {
|
||||||
|
|
@ -385,34 +393,94 @@ body { font-family: var(--font); background: var(--bg); color: var(--text1); -we
|
||||||
padding: 12px 10px;
|
padding: 12px 10px;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
transition: border-color 0.15s;
|
transition: border-color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-stat-card,
|
||||||
|
.dashboard-tile {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
box-sizing: border-box;
|
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;
|
display: flex;
|
||||||
gap: 8px;
|
align-items: flex-end;
|
||||||
margin-bottom: 16px;
|
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;
|
flex: 1;
|
||||||
min-width: 0;
|
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) {
|
@media (min-width: 1024px) {
|
||||||
.dashboard-stat-grid {
|
.dashboard-stat-grid,
|
||||||
|
.dashboard-tile-grid {
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-summary-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-summary-row > .card {
|
|
||||||
flex: unset;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
31
frontend/src/components/DashboardSection.jsx
Normal file
31
frontend/src/components/DashboardSection.jsx
Normal file
|
|
@ -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 (
|
||||||
|
<section className={`dashboard-section ${className}`.trim()}>
|
||||||
|
{showHeader && (
|
||||||
|
<header className="dashboard-section__header">
|
||||||
|
<div className="dashboard-section__headline">
|
||||||
|
{title ? <h2 className="dashboard-section__title">{title}</h2> : null}
|
||||||
|
{description ? (
|
||||||
|
<p className="dashboard-section__description">{description}</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
{headerRight ? (
|
||||||
|
<div className="dashboard-section__actions">{headerRight}</div>
|
||||||
|
) : null}
|
||||||
|
</header>
|
||||||
|
)}
|
||||||
|
<div className={`dashboard-section__body ${bodyClassName}`.trim()}>{children}</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
29
frontend/src/components/DashboardTile.jsx
Normal file
29
frontend/src/components/DashboardTile.jsx
Normal file
|
|
@ -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 (
|
||||||
|
<div
|
||||||
|
className={`dashboard-tile ${className}`.trim()}
|
||||||
|
style={{
|
||||||
|
'--tile-sm': String(sm),
|
||||||
|
'--tile-lg': String(lg)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useState, useEffect, useRef } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { Check, ChevronRight, Brain } from 'lucide-react'
|
import { Check, Brain } from 'lucide-react'
|
||||||
import {
|
import {
|
||||||
LineChart, Line, XAxis, YAxis, Tooltip,
|
LineChart, Line, XAxis, YAxis, Tooltip,
|
||||||
ResponsiveContainer, CartesianGrid
|
ResponsiveContainer, CartesianGrid
|
||||||
|
|
@ -13,10 +13,17 @@ import EmailVerificationBanner from '../components/EmailVerificationBanner'
|
||||||
import TrainingTypeDistribution from '../components/TrainingTypeDistribution'
|
import TrainingTypeDistribution from '../components/TrainingTypeDistribution'
|
||||||
import SleepWidget from '../components/SleepWidget'
|
import SleepWidget from '../components/SleepWidget'
|
||||||
import RestDaysWidget from '../components/RestDaysWidget'
|
import RestDaysWidget from '../components/RestDaysWidget'
|
||||||
import { getInterpretation, getStatusColor, getStatusBg } from '../utils/interpret'
|
|
||||||
import Markdown from '../utils/Markdown'
|
import Markdown from '../utils/Markdown'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import 'dayjs/locale/de'
|
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')
|
dayjs.locale('de')
|
||||||
|
|
||||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -144,14 +151,37 @@ function Pill({ label, value, status, sub }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Stat Card ─────────────────────────────────────────────────────────────────
|
// ── 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
|
const deltaColor = delta==null ? null
|
||||||
: (deltaGoodWhenNeg ? delta<0 : delta>0) ? 'var(--accent)' : 'var(--warn)'
|
: (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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className="dashboard-stat-card"
|
className="dashboard-stat-card"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
style={{ cursor: onClick ? 'pointer' : 'default' }}
|
style={{
|
||||||
|
cursor: onClick ? 'pointer' : 'default',
|
||||||
|
'--tile-sm': String(sm),
|
||||||
|
'--tile-lg': String(lg)
|
||||||
|
}}
|
||||||
onMouseEnter={e=>onClick&&(e.currentTarget.style.borderColor='var(--accent)')}
|
onMouseEnter={e=>onClick&&(e.currentTarget.style.borderColor='var(--accent)')}
|
||||||
onMouseLeave={e=>onClick&&(e.currentTarget.style.borderColor='var(--border)')}>
|
onMouseLeave={e=>onClick&&(e.currentTarget.style.borderColor='var(--border)')}>
|
||||||
<div style={{fontSize:18,marginBottom:4}}>{icon}</div>
|
<div style={{fontSize:18,marginBottom:4}}>{icon}</div>
|
||||||
|
|
@ -260,7 +290,6 @@ export default function Dashboard() {
|
||||||
const runPipeline = async () => {
|
const runPipeline = async () => {
|
||||||
setPipelineLoading(true); setPipelineError(null)
|
setPipelineLoading(true); setPipelineError(null)
|
||||||
try {
|
try {
|
||||||
const pid = localStorage.getItem('mitai-jinkendo_active_profile')||''
|
|
||||||
await api.insightPipeline()
|
await api.insightPipeline()
|
||||||
await load()
|
await load()
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
|
|
@ -268,12 +297,7 @@ export default function Dashboard() {
|
||||||
} finally { setPipelineLoading(false) }
|
} finally { setPipelineLoading(false) }
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(()=>{
|
useEffect(()=>{ load() },[])
|
||||||
console.log('[Dashboard] Component mounted, loading data...')
|
|
||||||
load()
|
|
||||||
},[])
|
|
||||||
|
|
||||||
console.log('[Dashboard] Rendering, loading=', loading, 'activeProfile=', activeProfile?.name)
|
|
||||||
|
|
||||||
if (loading) return <div className="empty-state"><div className="spinner"/></div>
|
if (loading) return <div className="empty-state"><div className="spinner"/></div>
|
||||||
|
|
||||||
|
|
@ -317,7 +341,11 @@ export default function Dashboard() {
|
||||||
|
|
||||||
const hasAnyData = latestW||latestCal||nutrition.length>0
|
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 (
|
return (
|
||||||
<div className="dashboard-page">
|
<div className="dashboard-page">
|
||||||
|
|
@ -349,46 +377,54 @@ export default function Dashboard() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{hasAnyData && <>
|
{hasAnyData && <>
|
||||||
{/* Quick weight entry */}
|
<DashboardSection
|
||||||
<div className="card section-gap">
|
title="Gewicht heute"
|
||||||
<div style={{display:'flex',alignItems:'center',justifyContent:'space-between',marginBottom:10}}>
|
description="Tageswert erfassen – Grundlage für Trends und Ziele."
|
||||||
<div style={{fontWeight:600,fontSize:14}}>⚖️ Gewicht heute</div>
|
headerRight={
|
||||||
<button style={{background:'none',border:'none',fontSize:12,color:'var(--accent)',cursor:'pointer'}}
|
<button type="button" className="btn btn-secondary"
|
||||||
onClick={()=>nav('/weight')}>
|
style={{ fontSize: 12, padding: '6px 12px' }}
|
||||||
|
onClick={() => nav('/weight')}>
|
||||||
Alle Einträge →
|
Alle Einträge →
|
||||||
</button>
|
</button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="card section-gap">
|
||||||
|
<QuickWeight onSaved={load}/>
|
||||||
</div>
|
</div>
|
||||||
<QuickWeight onSaved={load}/>
|
</DashboardSection>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Key metrics — Mobile: flex-wrap; Desktop: 4-spaltig (RESPONSIVE_UI P3) */}
|
<DashboardSection
|
||||||
<div className="dashboard-stat-grid">
|
title="Kennzahlen"
|
||||||
<StatCard icon="⚖️" label="Gewicht" value={latestW?.weight??'–'} unit="kg"
|
description="Aktuelle Messwerte und Ernährungs-Schnitt (7 Tage)."
|
||||||
delta={wDelta} deltaGoodWhenNeg={true}
|
>
|
||||||
sub={latestW ? dayjs(latestW.date).format('DD.MM.') : '–'}
|
<div className={dashboardStatGridClassName(DASHBOARD_TILE_GRID_COLS.mobile)}>
|
||||||
onClick={()=>nav('/history')} color="#378ADD"/>
|
<StatCard icon="⚖️" label="Gewicht" value={latestW?.weight??'–'} unit="kg"
|
||||||
{latestCal?.body_fat_pct && <StatCard icon="🫧" label="Körperfett" value={latestCal.body_fat_pct} unit="%"
|
delta={wDelta} deltaGoodWhenNeg={true}
|
||||||
delta={bfDelta} deltaGoodWhenNeg={true}
|
sub={latestW ? dayjs(latestW.date).format('DD.MM.') : '–'}
|
||||||
sub={bfCat?.label}
|
onClick={()=>nav('/history')} color="#378ADD"/>
|
||||||
onClick={()=>nav('/history',{state:{tab:'body'}})} color={bfCat?.color}/>}
|
{latestCal?.body_fat_pct && <StatCard icon="🫧" label="Körperfett" value={latestCal.body_fat_pct} unit="%"
|
||||||
{latestCal?.lean_mass && <StatCard icon="💪" label="Magermasse" value={latestCal.lean_mass} unit="kg"
|
delta={bfDelta} deltaGoodWhenNeg={true}
|
||||||
sub={latestCal.date ? dayjs(latestCal.date).format('DD.MM.') : '–'}
|
sub={bfCat?.label}
|
||||||
onClick={()=>nav('/history',{state:{tab:'body'}})}/>}
|
onClick={()=>nav('/history',{state:{tab:'body'}})} color={bfCat?.color}/>}
|
||||||
{avgKcal && <StatCard icon="🍽️" label="Ø Kalorien" value={avgKcal} unit="kcal"
|
{latestCal?.lean_mass && <StatCard icon="💪" label="Magermasse" value={latestCal.lean_mass} unit="kg"
|
||||||
sub="letzte 7 Tage" onClick={()=>nav('/history',{state:{tab:'nutrition'}})} color="#EF9F27"/>}
|
sub={latestCal.date ? dayjs(latestCal.date).format('DD.MM.') : '–'}
|
||||||
</div>
|
onClick={()=>nav('/history',{state:{tab:'body'}})}/>}
|
||||||
|
{avgKcal && <StatCard icon="🍽️" label="Ø Kalorien" value={avgKcal} unit="kcal"
|
||||||
{/* Status pills */}
|
sub="letzte 7 Tage" onClick={()=>nav('/history',{state:{tab:'nutrition'}})} color="#EF9F27"/>}
|
||||||
{pills.length > 0 && (
|
|
||||||
<div style={{display:'flex',gap:6,flexWrap:'wrap',marginBottom:16}}>
|
|
||||||
{pills.map((p,i)=><Pill key={i} {...p}/>)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
{pills.length > 0 && (
|
||||||
|
<div className="dashboard-pill-row">
|
||||||
|
{pills.map((p,i)=><Pill key={i} {...p}/>)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DashboardSection>
|
||||||
|
|
||||||
{/* Goals progress */}
|
|
||||||
{(activeProfile?.goal_weight||activeProfile?.goal_bf_pct) && latestW && (
|
{(activeProfile?.goal_weight||activeProfile?.goal_bf_pct) && latestW && (
|
||||||
<div className="card section-gap" style={{marginBottom:16}}>
|
<DashboardSection
|
||||||
<div style={{fontWeight:600,fontSize:13,marginBottom:10}}>🎯 Ziele</div>
|
title="Profil-Ziele"
|
||||||
|
description="Fortschritt zu den Zielwerten in deinem Profil."
|
||||||
|
>
|
||||||
|
<div className="card section-gap">
|
||||||
{activeProfile?.goal_weight && latestW && (()=>{
|
{activeProfile?.goal_weight && latestW && (()=>{
|
||||||
const start = Math.max(...weights.map(w=>w.weight))
|
const start = Math.max(...weights.map(w=>w.weight))
|
||||||
const curr = latestW.weight
|
const curr = latestW.weight
|
||||||
|
|
@ -429,134 +465,167 @@ export default function Dashboard() {
|
||||||
)
|
)
|
||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
|
</DashboardSection>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Combined chart */}
|
|
||||||
{(weights.length>2||nutrition.length>2) && (
|
{(weights.length>2||nutrition.length>2) && (
|
||||||
<div className="card section-gap" style={{marginBottom:16}}>
|
<DashboardSection
|
||||||
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:8}}>
|
title="Trends"
|
||||||
<div style={{fontWeight:600,fontSize:13}}>📊 Kalorien + Gewicht (30 Tage)</div>
|
description="Kalorien und Gewicht der letzten 30 Tage."
|
||||||
<button style={{background:'none',border:'none',fontSize:12,color:'var(--accent)',cursor:'pointer'}}
|
headerRight={
|
||||||
|
<button type="button" className="btn btn-secondary" style={{ fontSize: 12, padding: '6px 12px' }}
|
||||||
onClick={()=>nav('/history',{state:{tab:'body'}})}>
|
onClick={()=>nav('/history',{state:{tab:'body'}})}>
|
||||||
Details →
|
Details →
|
||||||
</button>
|
</button>
|
||||||
</div>
|
}
|
||||||
<ComboChart weights={weights} nutrition={nutrition}/>
|
>
|
||||||
<div style={{display:'flex',gap:16,justifyContent:'center',marginTop:6,fontSize:10,color:'var(--text3)'}}>
|
<DashboardTile>
|
||||||
<span><span style={{display:'inline-block',width:12,height:2,background:'#EF9F27',verticalAlign:'middle',marginRight:3}}/>Ø Kalorien</span>
|
<div className="card section-gap">
|
||||||
<span><span style={{display:'inline-block',width:12,height:2,background:'#378ADD',verticalAlign:'middle',marginRight:3}}/>Gewicht</span>
|
<ComboChart weights={weights} nutrition={nutrition}/>
|
||||||
</div>
|
<div style={{display:'flex',gap:16,justifyContent:'center',marginTop:6,fontSize:10,color:'var(--text3)'}}>
|
||||||
</div>
|
<span><span style={{display:'inline-block',width:12,height:2,background:'#EF9F27',verticalAlign:'middle',marginRight:3}}/>Ø Kalorien</span>
|
||||||
|
<span><span style={{display:'inline-block',width:12,height:2,background:'#378ADD',verticalAlign:'middle',marginRight:3}}/>Gewicht</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DashboardTile>
|
||||||
|
</DashboardSection>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Activity + Nutrition summary row */}
|
{(showNutrSummary || showActSummary) && (
|
||||||
<div className="dashboard-summary-row">
|
<DashboardSection
|
||||||
{(avgKcal||avgProtein) && (
|
title="Ernährung & Aktivität"
|
||||||
<div className="card" style={{flex:1,cursor:'pointer'}} onClick={()=>nav('/history',{state:{tab:'nutrition'}})}>
|
description="Kurzüberblick; volle Verläufe unter Historie."
|
||||||
<div style={{fontWeight:600,fontSize:12,marginBottom:8,color:'var(--text3)'}}>🍽️ ERNÄHRUNG (Ø 7T)</div>
|
>
|
||||||
{avgKcal && <div style={{fontSize:16,fontWeight:700,color:'#EF9F27'}}>{avgKcal} kcal</div>}
|
<div className={`dashboard-summary-row ${dashboardTileGridClassName(DASHBOARD_TILE_GRID_COLS.mobile)}`}>
|
||||||
{avgProtein && <div style={{fontSize:13,fontWeight:600,
|
{showNutrSummary && (
|
||||||
color:proteinOk?'var(--accent)':'var(--warn)'}}>
|
<DashboardTile spanMobile={summarySpanM} spanDesktop={summarySpanD}>
|
||||||
{avgProtein}g Protein {proteinOk?'✓':'⚠️'}
|
<div className="card" style={{ cursor: 'pointer', height: '100%' }} onClick={()=>nav('/history',{state:{tab:'nutrition'}})}>
|
||||||
</div>}
|
<div style={{fontWeight:600,fontSize:12,marginBottom:8,color:'var(--text3)'}}>🍽️ ERNÄHRUNG (Ø 7T)</div>
|
||||||
<div style={{fontSize:10,color:'var(--text3)',marginTop:4}}>→ Verlauf Ernährung</div>
|
{avgKcal && <div style={{fontSize:16,fontWeight:700,color:'#EF9F27'}}>{avgKcal} kcal</div>}
|
||||||
|
{avgProtein && <div style={{fontSize:13,fontWeight:600,
|
||||||
|
color:proteinOk?'var(--accent)':'var(--warn)'}}>
|
||||||
|
{avgProtein}g Protein {proteinOk?'✓':'⚠️'}
|
||||||
|
</div>}
|
||||||
|
<div style={{fontSize:10,color:'var(--text3)',marginTop:4}}>→ Verlauf Ernährung</div>
|
||||||
|
</div>
|
||||||
|
</DashboardTile>
|
||||||
|
)}
|
||||||
|
{showActSummary && (
|
||||||
|
<DashboardTile spanMobile={summarySpanM} spanDesktop={summarySpanD}>
|
||||||
|
<div className="card" style={{ cursor: 'pointer', height: '100%' }} onClick={()=>nav('/history',{state:{tab:'activity'}})}>
|
||||||
|
<div style={{fontWeight:600,fontSize:12,marginBottom:8,color:'var(--text3)'}}>🏋️ AKTIVITÄT (7T)</div>
|
||||||
|
<div style={{fontSize:16,fontWeight:700,color:'#EF9F27'}}>{actKcal} kcal</div>
|
||||||
|
<div style={{fontSize:13,color:'var(--text2)'}}>{recentAct.length} Trainings</div>
|
||||||
|
<div style={{fontSize:10,color:'var(--text3)',marginTop:4}}>→ Verlauf Aktivität</div>
|
||||||
|
</div>
|
||||||
|
</DashboardTile>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</DashboardSection>
|
||||||
{actKcal!=null && (
|
)}
|
||||||
<div className="card" style={{flex:1,cursor:'pointer'}} onClick={()=>nav('/history',{state:{tab:'activity'}})}>
|
|
||||||
<div style={{fontWeight:600,fontSize:12,marginBottom:8,color:'var(--text3)'}}>🏋️ AKTIVITÄT (7T)</div>
|
|
||||||
<div style={{fontSize:16,fontWeight:700,color:'#EF9F27'}}>{actKcal} kcal</div>
|
|
||||||
<div style={{fontSize:13,color:'var(--text2)'}}>{recentAct.length} Trainings</div>
|
|
||||||
<div style={{fontSize:10,color:'var(--text3)',marginTop:4}}>→ Verlauf Aktivität</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Sleep Widget */}
|
<DashboardSection
|
||||||
<div style={{marginBottom:16}}>
|
title="Erholung"
|
||||||
<SleepWidget/>
|
description="Schlaf und Ruhetage im Überblick."
|
||||||
</div>
|
>
|
||||||
|
<div className={`dashboard-erholung-grid ${dashboardTileGridClassName(DASHBOARD_TILE_GRID_COLS.mobile)}`}>
|
||||||
|
<DashboardTile spanMobile={1} spanDesktop={2}>
|
||||||
|
<SleepWidget/>
|
||||||
|
</DashboardTile>
|
||||||
|
<DashboardTile spanMobile={1} spanDesktop={2}>
|
||||||
|
<RestDaysWidget/>
|
||||||
|
</DashboardTile>
|
||||||
|
</div>
|
||||||
|
</DashboardSection>
|
||||||
|
|
||||||
{/* Rest Days Widget */}
|
|
||||||
<div style={{marginBottom:16}}>
|
|
||||||
<RestDaysWidget/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Training Type Distribution */}
|
|
||||||
{activities.length > 0 && (
|
{activities.length > 0 && (
|
||||||
<div className="card section-gap" style={{marginBottom:16}}>
|
<DashboardSection
|
||||||
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:12}}>
|
title="Training"
|
||||||
<div style={{fontWeight:600,fontSize:13}}>🏋️ Trainingstyp-Verteilung</div>
|
description="Verteilung der Trainingstypen (28 Tage)."
|
||||||
<button style={{background:'none',border:'none',fontSize:12,color:'var(--accent)',cursor:'pointer'}}
|
headerRight={
|
||||||
|
<button type="button" className="btn btn-secondary" style={{ fontSize: 12, padding: '6px 12px' }}
|
||||||
onClick={()=>nav('/activity')}>
|
onClick={()=>nav('/activity')}>
|
||||||
Details →
|
Details →
|
||||||
</button>
|
</button>
|
||||||
</div>
|
}
|
||||||
<TrainingTypeDistribution days={28} />
|
>
|
||||||
</div>
|
<DashboardTile>
|
||||||
|
<div className="card section-gap">
|
||||||
|
<TrainingTypeDistribution days={28} />
|
||||||
|
</div>
|
||||||
|
</DashboardTile>
|
||||||
|
</DashboardSection>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Goals Preview */}
|
<DashboardSection
|
||||||
<div className="card section-gap" style={{marginBottom:16,cursor:'pointer'}}
|
title="Ziele & Fokus"
|
||||||
onClick={()=>nav('/goals')}>
|
description="Trainingsmodus, Schwerpunkte und konkrete Ziele für die KI."
|
||||||
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:12}}>
|
headerRight={
|
||||||
<div style={{fontWeight:600,fontSize:13}}>🎯 Ziele</div>
|
<button type="button" className="btn btn-secondary" style={{ fontSize: 12, padding: '6px 12px' }}
|
||||||
<button style={{background:'none',border:'none',fontSize:12,color:'var(--accent)',cursor:'pointer'}}
|
onClick={(e)=>{ e.stopPropagation(); nav('/goals') }}>
|
||||||
onClick={(e)=>{e.stopPropagation();nav('/goals')}}>
|
|
||||||
Verwalten →
|
Verwalten →
|
||||||
</button>
|
</button>
|
||||||
</div>
|
}
|
||||||
<div style={{fontSize:12,color:'var(--text2)',padding:'8px 0'}}>
|
>
|
||||||
Definiere deine Trainingsmodus und konkrete Ziele für bessere KI-Analysen
|
<DashboardTile>
|
||||||
</div>
|
<div className="card section-gap" style={{ cursor: 'pointer' }} onClick={()=>nav('/goals')}>
|
||||||
</div>
|
<div style={{fontSize:12,color:'var(--text2)',padding:'8px 0'}}>
|
||||||
|
Definiere deine Trainingsmodus und konkrete Ziele für bessere KI-Analysen
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DashboardTile>
|
||||||
|
</DashboardSection>
|
||||||
|
|
||||||
{/* Latest AI insight */}
|
<DashboardSection
|
||||||
<div className="card section-gap">
|
title="KI-Auswertung"
|
||||||
<div style={{display:'flex',alignItems:'center',justifyContent:'space-between',marginBottom:8}}>
|
description="Mehrstufige Pipeline und letzte Zusammenfassung."
|
||||||
<div style={{fontWeight:600,fontSize:13}}>🤖 KI-Auswertung</div>
|
headerRight={
|
||||||
<button className="btn btn-secondary" style={{fontSize:11,padding:'4px 10px'}}
|
<button type="button" className="btn btn-secondary" style={{ fontSize: 11, padding: '4px 10px' }}
|
||||||
onClick={()=>nav('/analysis')}>
|
onClick={()=>nav('/analysis')}>
|
||||||
<Brain size={11}/> Analysen →
|
<Brain size={11}/> Analysen →
|
||||||
</button>
|
</button>
|
||||||
</div>
|
}
|
||||||
{/* Pipeline trigger */}
|
>
|
||||||
<button className="btn btn-primary btn-full" style={{marginBottom:10}}
|
<DashboardTile>
|
||||||
onClick={runPipeline} disabled={pipelineLoading}>
|
<div className="card section-gap">
|
||||||
{pipelineLoading
|
<button type="button" className="btn btn-primary btn-full" style={{marginBottom:10}}
|
||||||
? <><div className="spinner" style={{width:13,height:13}}/> Analyse läuft… (3 Stufen)</>
|
onClick={runPipeline} disabled={pipelineLoading}>
|
||||||
: <><Brain size={13}/> 🔬 Mehrstufige Analyse starten</>}
|
{pipelineLoading
|
||||||
</button>
|
? <><div className="spinner" style={{width:13,height:13}}/> Analyse läuft… (3 Stufen)</>
|
||||||
{pipelineError && <div style={{fontSize:12,color:'#D85A30',marginBottom:8}}>{pipelineError}</div>}
|
: <><Brain size={13}/> 🔬 Mehrstufige Analyse starten</>}
|
||||||
|
</button>
|
||||||
|
{pipelineError && <div style={{fontSize:12,color:'#D85A30',marginBottom:8}}>{pipelineError}</div>}
|
||||||
|
|
||||||
{latestInsight ? (
|
{latestInsight ? (
|
||||||
<>
|
<>
|
||||||
<div style={{fontSize:11,color:'var(--text3)',marginBottom:6}}>
|
<div style={{fontSize:11,color:'var(--text3)',marginBottom:6}}>
|
||||||
Letzte Analyse: {dayjs(latestInsight.created).format('DD. MMMM YYYY, HH:mm')}
|
Letzte Analyse: {dayjs(latestInsight.created).format('DD. MMMM YYYY, HH:mm')}
|
||||||
</div>
|
</div>
|
||||||
<div style={{maxHeight: showInsight?'none':120, overflow:'hidden', position:'relative'}}>
|
<div style={{maxHeight: showInsight?'none':120, overflow:'hidden', position:'relative'}}>
|
||||||
<Markdown text={latestInsight.content}/>
|
<Markdown text={latestInsight.content}/>
|
||||||
{!showInsight && (
|
{!showInsight && (
|
||||||
<div style={{position:'absolute',bottom:0,left:0,right:0,height:40,
|
<div style={{position:'absolute',bottom:0,left:0,right:0,height:40,
|
||||||
background:'linear-gradient(transparent,var(--surface))'}}/>
|
background:'linear-gradient(transparent,var(--surface))'}}/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<button style={{background:'none',border:'none',cursor:'pointer',
|
<button type="button" style={{background:'none',border:'none',cursor:'pointer',
|
||||||
fontSize:12,color:'var(--accent)',marginTop:6,padding:0}}
|
fontSize:12,color:'var(--accent)',marginTop:6,padding:0}}
|
||||||
onClick={()=>setShowInsight(s=>!s)}>
|
onClick={()=>setShowInsight(s=>!s)}>
|
||||||
{showInsight?'▲ Weniger anzeigen':'▼ Vollständig anzeigen'}
|
{showInsight?'▲ Weniger anzeigen':'▼ Vollständig anzeigen'}
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div style={{fontSize:13,color:'var(--text3)',padding:'8px 0'}}>
|
<div style={{fontSize:13,color:'var(--text3)',padding:'8px 0'}}>
|
||||||
Noch keine KI-Auswertung vorhanden.
|
Noch keine KI-Auswertung vorhanden.
|
||||||
<button className="btn btn-primary" style={{marginTop:8,display:'block',fontSize:12}}
|
<button type="button" className="btn btn-primary" style={{marginTop:8,display:'block',fontSize:12}}
|
||||||
onClick={()=>nav('/analysis')}>
|
onClick={()=>nav('/analysis')}>
|
||||||
Erste Analyse erstellen
|
Erste Analyse erstellen
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</DashboardTile>
|
||||||
</div>
|
</DashboardSection>
|
||||||
</>}
|
</>}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
26
frontend/src/utils/dashboardLayout.js
Normal file
26
frontend/src/utils/dashboardLayout.js
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user