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;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
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 { 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 (
|
||||
<div
|
||||
className="dashboard-stat-card"
|
||||
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)')}
|
||||
onMouseLeave={e=>onClick&&(e.currentTarget.style.borderColor='var(--border)')}>
|
||||
<div style={{fontSize:18,marginBottom:4}}>{icon}</div>
|
||||
|
|
@ -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 <div className="empty-state"><div className="spinner"/></div>
|
||||
|
||||
|
|
@ -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 (
|
||||
<div className="dashboard-page">
|
||||
|
|
@ -349,46 +377,54 @@ export default function Dashboard() {
|
|||
)}
|
||||
|
||||
{hasAnyData && <>
|
||||
{/* Quick weight entry */}
|
||||
<div className="card section-gap">
|
||||
<div style={{display:'flex',alignItems:'center',justifyContent:'space-between',marginBottom:10}}>
|
||||
<div style={{fontWeight:600,fontSize:14}}>⚖️ Gewicht heute</div>
|
||||
<button style={{background:'none',border:'none',fontSize:12,color:'var(--accent)',cursor:'pointer'}}
|
||||
onClick={()=>nav('/weight')}>
|
||||
<DashboardSection
|
||||
title="Gewicht heute"
|
||||
description="Tageswert erfassen – Grundlage für Trends und Ziele."
|
||||
headerRight={
|
||||
<button type="button" className="btn btn-secondary"
|
||||
style={{ fontSize: 12, padding: '6px 12px' }}
|
||||
onClick={() => nav('/weight')}>
|
||||
Alle Einträge →
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<div className="card section-gap">
|
||||
<QuickWeight onSaved={load}/>
|
||||
</div>
|
||||
<QuickWeight onSaved={load}/>
|
||||
</div>
|
||||
</DashboardSection>
|
||||
|
||||
{/* Key metrics — Mobile: flex-wrap; Desktop: 4-spaltig (RESPONSIVE_UI P3) */}
|
||||
<div className="dashboard-stat-grid">
|
||||
<StatCard icon="⚖️" label="Gewicht" value={latestW?.weight??'–'} unit="kg"
|
||||
delta={wDelta} deltaGoodWhenNeg={true}
|
||||
sub={latestW ? dayjs(latestW.date).format('DD.MM.') : '–'}
|
||||
onClick={()=>nav('/history')} color="#378ADD"/>
|
||||
{latestCal?.body_fat_pct && <StatCard icon="🫧" label="Körperfett" value={latestCal.body_fat_pct} unit="%"
|
||||
delta={bfDelta} deltaGoodWhenNeg={true}
|
||||
sub={bfCat?.label}
|
||||
onClick={()=>nav('/history',{state:{tab:'body'}})} color={bfCat?.color}/>}
|
||||
{latestCal?.lean_mass && <StatCard icon="💪" label="Magermasse" value={latestCal.lean_mass} unit="kg"
|
||||
sub={latestCal.date ? dayjs(latestCal.date).format('DD.MM.') : '–'}
|
||||
onClick={()=>nav('/history',{state:{tab:'body'}})}/>}
|
||||
{avgKcal && <StatCard icon="🍽️" label="Ø Kalorien" value={avgKcal} unit="kcal"
|
||||
sub="letzte 7 Tage" onClick={()=>nav('/history',{state:{tab:'nutrition'}})} color="#EF9F27"/>}
|
||||
</div>
|
||||
|
||||
{/* Status pills */}
|
||||
{pills.length > 0 && (
|
||||
<div style={{display:'flex',gap:6,flexWrap:'wrap',marginBottom:16}}>
|
||||
{pills.map((p,i)=><Pill key={i} {...p}/>)}
|
||||
<DashboardSection
|
||||
title="Kennzahlen"
|
||||
description="Aktuelle Messwerte und Ernährungs-Schnitt (7 Tage)."
|
||||
>
|
||||
<div className={dashboardStatGridClassName(DASHBOARD_TILE_GRID_COLS.mobile)}>
|
||||
<StatCard icon="⚖️" label="Gewicht" value={latestW?.weight??'–'} unit="kg"
|
||||
delta={wDelta} deltaGoodWhenNeg={true}
|
||||
sub={latestW ? dayjs(latestW.date).format('DD.MM.') : '–'}
|
||||
onClick={()=>nav('/history')} color="#378ADD"/>
|
||||
{latestCal?.body_fat_pct && <StatCard icon="🫧" label="Körperfett" value={latestCal.body_fat_pct} unit="%"
|
||||
delta={bfDelta} deltaGoodWhenNeg={true}
|
||||
sub={bfCat?.label}
|
||||
onClick={()=>nav('/history',{state:{tab:'body'}})} color={bfCat?.color}/>}
|
||||
{latestCal?.lean_mass && <StatCard icon="💪" label="Magermasse" value={latestCal.lean_mass} unit="kg"
|
||||
sub={latestCal.date ? dayjs(latestCal.date).format('DD.MM.') : '–'}
|
||||
onClick={()=>nav('/history',{state:{tab:'body'}})}/>}
|
||||
{avgKcal && <StatCard icon="🍽️" label="Ø Kalorien" value={avgKcal} unit="kcal"
|
||||
sub="letzte 7 Tage" onClick={()=>nav('/history',{state:{tab:'nutrition'}})} color="#EF9F27"/>}
|
||||
</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 && (
|
||||
<div className="card section-gap" style={{marginBottom:16}}>
|
||||
<div style={{fontWeight:600,fontSize:13,marginBottom:10}}>🎯 Ziele</div>
|
||||
<DashboardSection
|
||||
title="Profil-Ziele"
|
||||
description="Fortschritt zu den Zielwerten in deinem Profil."
|
||||
>
|
||||
<div className="card section-gap">
|
||||
{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() {
|
|||
)
|
||||
})()}
|
||||
</div>
|
||||
</DashboardSection>
|
||||
)}
|
||||
|
||||
{/* Combined chart */}
|
||||
{(weights.length>2||nutrition.length>2) && (
|
||||
<div className="card section-gap" style={{marginBottom:16}}>
|
||||
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:8}}>
|
||||
<div style={{fontWeight:600,fontSize:13}}>📊 Kalorien + Gewicht (30 Tage)</div>
|
||||
<button style={{background:'none',border:'none',fontSize:12,color:'var(--accent)',cursor:'pointer'}}
|
||||
<DashboardSection
|
||||
title="Trends"
|
||||
description="Kalorien und Gewicht der letzten 30 Tage."
|
||||
headerRight={
|
||||
<button type="button" className="btn btn-secondary" style={{ fontSize: 12, padding: '6px 12px' }}
|
||||
onClick={()=>nav('/history',{state:{tab:'body'}})}>
|
||||
Details →
|
||||
</button>
|
||||
</div>
|
||||
<ComboChart weights={weights} nutrition={nutrition}/>
|
||||
<div style={{display:'flex',gap:16,justifyContent:'center',marginTop:6,fontSize:10,color:'var(--text3)'}}>
|
||||
<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>
|
||||
<div className="card section-gap">
|
||||
<ComboChart weights={weights} nutrition={nutrition}/>
|
||||
<div style={{display:'flex',gap:16,justifyContent:'center',marginTop:6,fontSize:10,color:'var(--text3)'}}>
|
||||
<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 */}
|
||||
<div className="dashboard-summary-row">
|
||||
{(avgKcal||avgProtein) && (
|
||||
<div className="card" style={{flex:1,cursor:'pointer'}} onClick={()=>nav('/history',{state:{tab:'nutrition'}})}>
|
||||
<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>}
|
||||
{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>
|
||||
{(showNutrSummary || showActSummary) && (
|
||||
<DashboardSection
|
||||
title="Ernährung & Aktivität"
|
||||
description="Kurzüberblick; volle Verläufe unter Historie."
|
||||
>
|
||||
<div className={`dashboard-summary-row ${dashboardTileGridClassName(DASHBOARD_TILE_GRID_COLS.mobile)}`}>
|
||||
{showNutrSummary && (
|
||||
<DashboardTile spanMobile={summarySpanM} spanDesktop={summarySpanD}>
|
||||
<div className="card" style={{ cursor: 'pointer', height: '100%' }} onClick={()=>nav('/history',{state:{tab:'nutrition'}})}>
|
||||
<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>}
|
||||
{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>
|
||||
)}
|
||||
{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>
|
||||
</DashboardSection>
|
||||
)}
|
||||
|
||||
{/* Sleep Widget */}
|
||||
<div style={{marginBottom:16}}>
|
||||
<SleepWidget/>
|
||||
</div>
|
||||
<DashboardSection
|
||||
title="Erholung"
|
||||
description="Schlaf und Ruhetage im Überblick."
|
||||
>
|
||||
<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 && (
|
||||
<div className="card section-gap" style={{marginBottom:16}}>
|
||||
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:12}}>
|
||||
<div style={{fontWeight:600,fontSize:13}}>🏋️ Trainingstyp-Verteilung</div>
|
||||
<button style={{background:'none',border:'none',fontSize:12,color:'var(--accent)',cursor:'pointer'}}
|
||||
<DashboardSection
|
||||
title="Training"
|
||||
description="Verteilung der Trainingstypen (28 Tage)."
|
||||
headerRight={
|
||||
<button type="button" className="btn btn-secondary" style={{ fontSize: 12, padding: '6px 12px' }}
|
||||
onClick={()=>nav('/activity')}>
|
||||
Details →
|
||||
</button>
|
||||
</div>
|
||||
<TrainingTypeDistribution days={28} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<DashboardTile>
|
||||
<div className="card section-gap">
|
||||
<TrainingTypeDistribution days={28} />
|
||||
</div>
|
||||
</DashboardTile>
|
||||
</DashboardSection>
|
||||
)}
|
||||
|
||||
{/* Goals Preview */}
|
||||
<div className="card section-gap" style={{marginBottom:16,cursor:'pointer'}}
|
||||
onClick={()=>nav('/goals')}>
|
||||
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:12}}>
|
||||
<div style={{fontWeight:600,fontSize:13}}>🎯 Ziele</div>
|
||||
<button style={{background:'none',border:'none',fontSize:12,color:'var(--accent)',cursor:'pointer'}}
|
||||
onClick={(e)=>{e.stopPropagation();nav('/goals')}}>
|
||||
<DashboardSection
|
||||
title="Ziele & Fokus"
|
||||
description="Trainingsmodus, Schwerpunkte und konkrete Ziele für die KI."
|
||||
headerRight={
|
||||
<button type="button" className="btn btn-secondary" style={{ fontSize: 12, padding: '6px 12px' }}
|
||||
onClick={(e)=>{ e.stopPropagation(); nav('/goals') }}>
|
||||
Verwalten →
|
||||
</button>
|
||||
</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>
|
||||
<div className="card section-gap" style={{ cursor: 'pointer' }} onClick={()=>nav('/goals')}>
|
||||
<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 */}
|
||||
<div className="card section-gap">
|
||||
<div style={{display:'flex',alignItems:'center',justifyContent:'space-between',marginBottom:8}}>
|
||||
<div style={{fontWeight:600,fontSize:13}}>🤖 KI-Auswertung</div>
|
||||
<button className="btn btn-secondary" style={{fontSize:11,padding:'4px 10px'}}
|
||||
<DashboardSection
|
||||
title="KI-Auswertung"
|
||||
description="Mehrstufige Pipeline und letzte Zusammenfassung."
|
||||
headerRight={
|
||||
<button type="button" className="btn btn-secondary" style={{ fontSize: 11, padding: '4px 10px' }}
|
||||
onClick={()=>nav('/analysis')}>
|
||||
<Brain size={11}/> Analysen →
|
||||
</button>
|
||||
</div>
|
||||
{/* Pipeline trigger */}
|
||||
<button className="btn btn-primary btn-full" style={{marginBottom:10}}
|
||||
onClick={runPipeline} disabled={pipelineLoading}>
|
||||
{pipelineLoading
|
||||
? <><div className="spinner" style={{width:13,height:13}}/> Analyse läuft… (3 Stufen)</>
|
||||
: <><Brain size={13}/> 🔬 Mehrstufige Analyse starten</>}
|
||||
</button>
|
||||
{pipelineError && <div style={{fontSize:12,color:'#D85A30',marginBottom:8}}>{pipelineError}</div>}
|
||||
}
|
||||
>
|
||||
<DashboardTile>
|
||||
<div className="card section-gap">
|
||||
<button type="button" className="btn btn-primary btn-full" style={{marginBottom:10}}
|
||||
onClick={runPipeline} disabled={pipelineLoading}>
|
||||
{pipelineLoading
|
||||
? <><div className="spinner" style={{width:13,height:13}}/> Analyse läuft… (3 Stufen)</>
|
||||
: <><Brain size={13}/> 🔬 Mehrstufige Analyse starten</>}
|
||||
</button>
|
||||
{pipelineError && <div style={{fontSize:12,color:'#D85A30',marginBottom:8}}>{pipelineError}</div>}
|
||||
|
||||
{latestInsight ? (
|
||||
<>
|
||||
<div style={{fontSize:11,color:'var(--text3)',marginBottom:6}}>
|
||||
Letzte Analyse: {dayjs(latestInsight.created).format('DD. MMMM YYYY, HH:mm')}
|
||||
</div>
|
||||
<div style={{maxHeight: showInsight?'none':120, overflow:'hidden', position:'relative'}}>
|
||||
<Markdown text={latestInsight.content}/>
|
||||
{!showInsight && (
|
||||
<div style={{position:'absolute',bottom:0,left:0,right:0,height:40,
|
||||
background:'linear-gradient(transparent,var(--surface))'}}/>
|
||||
)}
|
||||
</div>
|
||||
<button style={{background:'none',border:'none',cursor:'pointer',
|
||||
fontSize:12,color:'var(--accent)',marginTop:6,padding:0}}
|
||||
onClick={()=>setShowInsight(s=>!s)}>
|
||||
{showInsight?'▲ Weniger anzeigen':'▼ Vollständig anzeigen'}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<div style={{fontSize:13,color:'var(--text3)',padding:'8px 0'}}>
|
||||
Noch keine KI-Auswertung vorhanden.
|
||||
<button className="btn btn-primary" style={{marginTop:8,display:'block',fontSize:12}}
|
||||
onClick={()=>nav('/analysis')}>
|
||||
Erste Analyse erstellen
|
||||
</button>
|
||||
{latestInsight ? (
|
||||
<>
|
||||
<div style={{fontSize:11,color:'var(--text3)',marginBottom:6}}>
|
||||
Letzte Analyse: {dayjs(latestInsight.created).format('DD. MMMM YYYY, HH:mm')}
|
||||
</div>
|
||||
<div style={{maxHeight: showInsight?'none':120, overflow:'hidden', position:'relative'}}>
|
||||
<Markdown text={latestInsight.content}/>
|
||||
{!showInsight && (
|
||||
<div style={{position:'absolute',bottom:0,left:0,right:0,height:40,
|
||||
background:'linear-gradient(transparent,var(--surface))'}}/>
|
||||
)}
|
||||
</div>
|
||||
<button type="button" style={{background:'none',border:'none',cursor:'pointer',
|
||||
fontSize:12,color:'var(--accent)',marginTop:6,padding:0}}
|
||||
onClick={()=>setShowInsight(s=>!s)}>
|
||||
{showInsight?'▲ Weniger anzeigen':'▼ Vollständig anzeigen'}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<div style={{fontSize:13,color:'var(--text3)',padding:'8px 0'}}>
|
||||
Noch keine KI-Auswertung vorhanden.
|
||||
<button type="button" className="btn btn-primary" style={{marginTop:8,display:'block',fontSize:12}}
|
||||
onClick={()=>nav('/analysis')}>
|
||||
Erste Analyse erstellen
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DashboardTile>
|
||||
</DashboardSection>
|
||||
</>}
|
||||
</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