feat: add compact evaluation tiles and body goals section
- Introduced a new `EvaluationTileGrid` component for displaying compact evaluation tiles with interactive features. - Added a `BodyGoalsStrip` component to showcase active body-related goals with progress indicators. - Enhanced CSS styles for the new components to ensure responsive design and improved user experience. - Updated the `BodySection` to integrate the new components and manage grouped goals effectively.
This commit is contained in:
parent
42ae796448
commit
157afd10b9
|
|
@ -199,6 +199,30 @@ body { font-family: var(--font); background: var(--bg); color: var(--text1); -we
|
|||
.page-title { font-size: 20px; font-weight: 700; margin-bottom: 16px; }
|
||||
|
||||
/* Verlauf: Mobile Tabs horizontale Leiste, Desktop vertikal links (P4 / RESPONSIVE_UI §5.2) */
|
||||
/* Körper-Verlauf: kompakte Bewertungs-Kacheln */
|
||||
.body-eval-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(148px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
.body-eval-tile {
|
||||
display: block;
|
||||
width: 100%;
|
||||
background: var(--surface2);
|
||||
border-radius: 10px;
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--border);
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
text-align: left;
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
.body-eval-tile:hover {
|
||||
border-color: var(--border2);
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.history-page__title {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ import { useProfile } from '../context/ProfileContext'
|
|||
import {
|
||||
LineChart, Line, BarChart, Bar,
|
||||
XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid,
|
||||
ReferenceLine, PieChart, Pie, Cell
|
||||
ReferenceLine, PieChart, Pie, Cell, ComposedChart
|
||||
} from 'recharts'
|
||||
import { ChevronRight, Brain, ChevronDown, ChevronUp, Trash2 } from 'lucide-react'
|
||||
import { api } from '../utils/api'
|
||||
import { photoMonthKey, photoSortKey, formatPhotoCaption } from '../utils/photoDisplay'
|
||||
import { getBfCategory } from '../utils/calc'
|
||||
import { getBfCategory, calcDerived } from '../utils/calc'
|
||||
import { getInterpretation, getStatusColor, getStatusBg } from '../utils/interpret'
|
||||
import Markdown from '../utils/Markdown'
|
||||
import TrainingTypeDistribution from '../components/TrainingTypeDistribution'
|
||||
|
|
@ -85,6 +85,93 @@ function RuleCard({ item }) {
|
|||
)
|
||||
}
|
||||
|
||||
/** Kompakte Bewertungs-Kacheln (z. B. Körper-Verlauf) */
|
||||
function EvaluationTileGrid({ items }) {
|
||||
const [open, setOpen] = useState(null)
|
||||
if (!items?.length) return null
|
||||
return (
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text3)', marginBottom: 8 }}>BEWERTUNG</div>
|
||||
<div className="body-eval-grid">
|
||||
{items.map((item, i) => {
|
||||
const color = getStatusColor(item.status)
|
||||
const expanded = open === i
|
||||
return (
|
||||
<button
|
||||
key={i}
|
||||
type="button"
|
||||
className="body-eval-tile"
|
||||
style={{ borderTop: `3px solid ${color}` }}
|
||||
onClick={() => setOpen(expanded ? null : i)}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 6, textAlign: 'left', width: '100%' }}>
|
||||
<span style={{ fontSize: 16, lineHeight: 1 }}>{item.icon}</span>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{
|
||||
fontSize: 9, fontWeight: 600, color, textTransform: 'uppercase', letterSpacing: '0.03em',
|
||||
}}>{item.category}</div>
|
||||
<div style={{ fontSize: 12, fontWeight: 600, lineHeight: 1.3, color: 'var(--text1)' }}>{item.title}</div>
|
||||
{expanded && (
|
||||
<div style={{ fontSize: 11, color: 'var(--text2)', lineHeight: 1.45, marginTop: 6 }}>{item.detail}</div>
|
||||
)}
|
||||
</div>
|
||||
{item.value && (
|
||||
<span style={{ fontSize: 13, fontWeight: 700, color, flexShrink: 0 }}>{item.value}</span>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function BodyGoalsStrip({ grouped }) {
|
||||
const nav = useNavigate()
|
||||
const goals = (grouped?.body || []).filter(g => g.status === 'active').slice(0, 4)
|
||||
if (!goals.length) return null
|
||||
return (
|
||||
<div className="card" style={{ marginBottom: 12 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text3)' }}>Körperbezogene Ziele</div>
|
||||
<button type="button" className="btn btn-secondary" style={{ fontSize: 10, padding: '2px 8px' }} onClick={() => nav('/goals')}>
|
||||
Ziele <ChevronRight size={10} />
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
||||
{goals.map(g => (
|
||||
<div
|
||||
key={g.id}
|
||||
style={{
|
||||
flex: '1 1 140px',
|
||||
background: 'var(--surface2)',
|
||||
borderRadius: 8,
|
||||
padding: '8px 10px',
|
||||
borderTop: `3px solid ${g.is_primary ? 'var(--accent)' : 'var(--border2)'}`,
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
fontSize: 11, fontWeight: 600, color: 'var(--text2)',
|
||||
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
|
||||
}}>{g.name || g.label_de || g.goal_type}</div>
|
||||
<div style={{ marginTop: 4, height: 6, background: 'var(--border)', borderRadius: 3, overflow: 'hidden' }}>
|
||||
<div style={{
|
||||
width: `${Math.min(100, Math.max(0, g.progress_pct ?? 0))}%`,
|
||||
height: '100%',
|
||||
background: 'var(--accent)',
|
||||
}} />
|
||||
</div>
|
||||
<div style={{ fontSize: 10, color: 'var(--text3)', marginTop: 4 }}>
|
||||
{Math.round(g.progress_pct ?? 0)}% · Ziel {g.target_value}{g.unit ? ` ${g.unit}` : ''}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function InsightBox({ insights, slugs, onRequest, loading }) {
|
||||
const [expanded, setExpanded] = useState(null)
|
||||
const relevant = insights?.filter(i=>slugs.includes(i.scope))||[]
|
||||
|
|
@ -154,9 +241,18 @@ function PeriodSelector({ value, onChange }) {
|
|||
// ── Body Section (Weight + Composition combined) ──────────────────────────────
|
||||
function BodySection({ weights, calipers, circs, profile, insights, onRequest, loadingSlug, filterActiveSlugs }) {
|
||||
const [period, setPeriod] = useState(90)
|
||||
const [groupedGoals, setGroupedGoals] = useState(null)
|
||||
const sex = profile?.sex||'m'
|
||||
const height = profile?.height||178
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
api.listGoalsGrouped()
|
||||
.then(g => { if (!cancelled) setGroupedGoals(g) })
|
||||
.catch(() => { if (!cancelled) setGroupedGoals({}) })
|
||||
return () => { cancelled = true }
|
||||
}, [])
|
||||
|
||||
const cutoff = dayjs().subtract(period,'day').format('YYYY-MM-DD')
|
||||
const filtW = [...(weights||[])].sort((a,b)=>a.date.localeCompare(b.date))
|
||||
.filter(d=>period===9999||d.date>=cutoff)
|
||||
|
|
@ -197,9 +293,9 @@ function BodySection({ weights, calipers, circs, profile, insights, onRequest, l
|
|||
return {label:`${days}T`,diff,count:per.length}
|
||||
}).filter(Boolean)
|
||||
|
||||
// ── Caliper chart ──
|
||||
// ── Caliper: nur KF% (Magermasse ist daraus abgeleitet — eigene zweite Achse entfällt) ──
|
||||
const bfCd = [...filtCal].filter(c=>c.body_fat_pct).reverse().map(c=>({
|
||||
date:fmtDate(c.date),bf:c.body_fat_pct,lean:c.lean_mass,fat:c.fat_mass
|
||||
date:fmtDate(c.date), bf:c.body_fat_pct,
|
||||
}))
|
||||
const latestCal = filtCal[0]
|
||||
const prevCal = filtCal[1]
|
||||
|
|
@ -207,11 +303,37 @@ function BodySection({ weights, calipers, circs, profile, insights, onRequest, l
|
|||
const latestW2 = filtW[filtW.length-1]
|
||||
const bfCat = latestCal?.body_fat_pct ? getBfCategory(latestCal.body_fat_pct,sex) : null
|
||||
|
||||
// ── Circ chart ──
|
||||
// ── Umfänge: chronologisch für Trends & Proportionen ──
|
||||
const circChron = [...filtCir].sort((a,b)=>a.date.localeCompare(b.date))
|
||||
const cirCd = [...filtCir].filter(c=>c.c_waist||c.c_hip).reverse().map(c=>({
|
||||
date:fmtDate(c.date),waist:c.c_waist,hip:c.c_hip,belly:c.c_belly
|
||||
}))
|
||||
|
||||
const propBase = circChron
|
||||
.filter(r => r.c_chest && r.c_waist)
|
||||
.map(r => ({
|
||||
date: fmtDate(r.date),
|
||||
vTaper: Math.round((r.c_chest - r.c_waist) * 10) / 10,
|
||||
belly: r.c_belly != null ? Math.round(r.c_belly * 10) / 10 : null,
|
||||
}))
|
||||
const propChartData = propBase.length >= 2 ? rollingAvg(propBase, 'vTaper', 3) : []
|
||||
const showBellyOnProp = propChartData.some(d => d.belly != null)
|
||||
|
||||
const fbFirst = { chest: null, waist: null, belly: null }
|
||||
for (const r of circChron) {
|
||||
if (fbFirst.chest == null && r.c_chest) fbFirst.chest = r.c_chest
|
||||
if (fbFirst.waist == null && r.c_waist) fbFirst.waist = r.c_waist
|
||||
if (fbFirst.belly == null && r.c_belly) fbFirst.belly = r.c_belly
|
||||
}
|
||||
const idxSeries = circChron.map(r => ({
|
||||
date: fmtDate(r.date),
|
||||
chestIdx: r.c_chest && fbFirst.chest ? Math.round((r.c_chest / fbFirst.chest) * 1000) / 10 : null,
|
||||
waistIdx: r.c_waist && fbFirst.waist ? Math.round((r.c_waist / fbFirst.waist) * 1000) / 10 : null,
|
||||
bellyIdx: r.c_belly && fbFirst.belly ? Math.round((r.c_belly / fbFirst.belly) * 1000) / 10 : null,
|
||||
}))
|
||||
const idxCount = idxSeries.filter(row => row.chestIdx != null || row.waistIdx != null || row.bellyIdx != null).length
|
||||
const idxOk = idxCount >= 2 && (fbFirst.chest || fbFirst.waist || fbFirst.belly)
|
||||
|
||||
// ── Indicators ──
|
||||
const whr = latestCir?.c_waist&&latestCir?.c_hip ? Math.round(latestCir.c_waist/latestCir.c_hip*100)/100 : null
|
||||
const whtr = latestCir?.c_waist&&height ? Math.round(latestCir.c_waist/height*100)/100 : null
|
||||
|
|
@ -223,12 +345,20 @@ function BodySection({ weights, calipers, circs, profile, insights, onRequest, l
|
|||
weight:latestW2?.weight
|
||||
}
|
||||
const rules = getInterpretation(combined, profile, prevCal||null)
|
||||
const derivedFFMI = calcDerived(combined, height)?.ffmi
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SectionHeader title="⚖️ Körper" lastUpdated={weights[0]?.date||calipers[0]?.date}/>
|
||||
<PeriodSelector value={period} onChange={setPeriod}/>
|
||||
|
||||
<BodyGoalsStrip grouped={groupedGoals} />
|
||||
|
||||
<p style={{ fontSize: 11, color: 'var(--text3)', lineHeight: 1.45, marginBottom: 12 }}>
|
||||
Hinweis: Diese Seite bündelt <strong>Körpermaße und -zusammensetzung</strong>. Trainingsbedingte Fitness (Belastung, Leistung, Ausdauer) findest du unter{' '}
|
||||
<strong>Verlauf → Aktivität</strong> — dort werden sportliche Trends ausgewertet, hier geht es um Silhouette, Zusammensetzung und Gesundheitsindikatoren.
|
||||
</p>
|
||||
|
||||
{/* Summary stats */}
|
||||
<div style={{display:'flex',gap:6,marginBottom:12,flexWrap:'wrap'}}>
|
||||
{latestW2 && <div style={{flex:1,minWidth:70,background:'var(--surface2)',borderRadius:8,padding:'8px 6px',textAlign:'center'}}>
|
||||
|
|
@ -243,6 +373,10 @@ function BodySection({ weights, calipers, circs, profile, insights, onRequest, l
|
|||
<div style={{fontSize:16,fontWeight:700,color:'#1D9E75'}}>{latestCal.lean_mass} kg</div>
|
||||
<div style={{fontSize:9,color:'var(--text3)'}}>Mager</div>
|
||||
</div>}
|
||||
{derivedFFMI != null && <div style={{flex:1,minWidth:70,background:'var(--surface2)',borderRadius:8,padding:'8px 6px',textAlign:'center'}}>
|
||||
<div style={{fontSize:16,fontWeight:700,color:'#378ADD'}}>{derivedFFMI}</div>
|
||||
<div style={{fontSize:9,color:'var(--text3)'}}>FFMI</div>
|
||||
</div>}
|
||||
{whr && <div style={{flex:1,minWidth:70,background:'var(--surface2)',borderRadius:8,padding:'8px 6px',textAlign:'center',
|
||||
borderTop:`3px solid ${whr<(sex==='m'?0.90:0.85)?'var(--accent)':'var(--warn)'}`}}>
|
||||
<div style={{fontSize:16,fontWeight:700,color:whr<(sex==='m'?0.90:0.85)?'var(--accent)':'var(--warn)'}}>{whr}</div>
|
||||
|
|
@ -316,43 +450,119 @@ function BodySection({ weights, calipers, circs, profile, insights, onRequest, l
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* KF + Magermasse chart */}
|
||||
{/* Körperfett — eine Zeitreihe (Magermasse steht oben als Kennzahl) */}
|
||||
{bfCd.length>=2 && (
|
||||
<div className="card" style={{marginBottom:12}}>
|
||||
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:8}}>
|
||||
<div style={{fontSize:12,fontWeight:600,color:'var(--text3)'}}>KF% + Magermasse</div>
|
||||
<div style={{fontSize:12,fontWeight:600,color:'var(--text3)'}}>Körperfett (Caliper)</div>
|
||||
<NavToCaliper/>
|
||||
</div>
|
||||
<ResponsiveContainer width="100%" height={170}>
|
||||
<LineChart data={bfCd} margin={{top:4,right:8,bottom:0,left:-20}}>
|
||||
<CartesianGrid stroke="var(--border)" strokeDasharray="3 3"/>
|
||||
<XAxis dataKey="date" tick={{fontSize:9,fill:'var(--text3)'}} tickLine={false}/>
|
||||
<YAxis yAxisId="bf" tick={{fontSize:9,fill:'var(--text3)'}} tickLine={false} domain={['auto','auto']}/>
|
||||
<YAxis yAxisId="lean" orientation="right" tick={{fontSize:9,fill:'var(--text3)'}} tickLine={false} domain={['auto','auto']}/>
|
||||
<YAxis tick={{fontSize:9,fill:'var(--text3)'}} tickLine={false} domain={['auto','auto']}/>
|
||||
<Tooltip contentStyle={{background:'var(--surface)',border:'1px solid var(--border)',borderRadius:8,fontSize:11}}
|
||||
formatter={(v,n)=>[`${v}${n==='bf'?'%':' kg'}`,n==='bf'?'KF%':'Mager']}/>
|
||||
{profile?.goal_bf_pct && <ReferenceLine yAxisId="bf" y={profile.goal_bf_pct}
|
||||
formatter={(v) => [`${v}%`, 'KF%']}/>
|
||||
{profile?.goal_bf_pct && <ReferenceLine y={profile.goal_bf_pct}
|
||||
stroke="#D85A30" strokeDasharray="5 3" strokeWidth={1.5}
|
||||
label={{value:`Ziel ${profile.goal_bf_pct}%`,fontSize:9,fill:'#D85A30',position:'right'}}/>}
|
||||
<Line yAxisId="bf" type="monotone" dataKey="bf" stroke="#D85A30" strokeWidth={2.5} dot={{r:4,fill:'#D85A30'}} name="bf"/>
|
||||
<Line yAxisId="lean" type="monotone" dataKey="lean" stroke="#1D9E75" strokeWidth={2} dot={{r:3,fill:'#1D9E75'}} name="lean"/>
|
||||
<Line type="monotone" dataKey="bf" stroke="#D85A30" strokeWidth={2.5} dot={{r:4,fill:'#D85A30'}} name="bf"/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
<div style={{display:'flex',gap:12,justifyContent:'center',marginTop:6,fontSize:10,color:'var(--text3)'}}>
|
||||
<span><span style={{display:'inline-block',width:12,height:2,background:'#D85A30',verticalAlign:'middle',marginRight:3}}/>KF%</span>
|
||||
<span><span style={{display:'inline-block',width:12,height:2,background:'#1D9E75',verticalAlign:'middle',marginRight:3}}/>Mager kg</span>
|
||||
{profile?.goal_bf_pct && <span><span style={{display:'inline-block',width:14,height:2,background:'#D85A30',verticalAlign:'middle',marginRight:3,borderTop:'2px dashed #D85A30'}}/>Ziel KF</span>}
|
||||
<div style={{fontSize:10,color:'var(--text3)',marginTop:6,lineHeight:1.4}}>
|
||||
Magermasse ergibt sich aus Gewicht und KF% — als zweite Kurve wäre sie redundant. Aktuelle Magermasse siehe Kennzahlen oben.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Circ trend */}
|
||||
{cirCd.length>=2 && (
|
||||
{/* Proportion: V-Taper vs. Bauch (Brust−Taille vs. Bauchumfang) */}
|
||||
{propChartData.length >= 2 && (
|
||||
<div className="card" style={{marginBottom:12}}>
|
||||
<div style={{display:'flex',justifyContent:'space-between',alignItems:'flex-start',marginBottom:8,gap:8}}>
|
||||
<div>
|
||||
<div style={{fontSize:12,fontWeight:600,color:'var(--text3)'}}>Silhouette & Proportion</div>
|
||||
<div style={{fontSize:10,color:'var(--text3)',lineHeight:1.45,marginTop:4}}>
|
||||
<strong>V-Taper (Brust − Taille)</strong> in cm: größer bedeutet stärkere Schulter-/Brustentwicklung relativ zur Taille.
|
||||
{showBellyOnProp && (
|
||||
<><strong> Bauch</strong> (rechte Achse): steigender Trend hier deutet eher auf Zunahme zentralen Umfangs hin — unabhängig von sportlicher Brustentwicklung.</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<NavToCircum/>
|
||||
</div>
|
||||
<ResponsiveContainer width="100%" height={200}>
|
||||
<ComposedChart data={propChartData} margin={{top:4,right:showBellyOnProp?4:8,bottom:0,left:-20}}>
|
||||
<CartesianGrid stroke="var(--border)" strokeDasharray="3 3"/>
|
||||
<XAxis dataKey="date" tick={{fontSize:9,fill:'var(--text3)'}} tickLine={false}/>
|
||||
<YAxis yAxisId="taper" tick={{fontSize:9,fill:'var(--text3)'}} tickLine={false} domain={['auto','auto']}/>
|
||||
{showBellyOnProp && (
|
||||
<YAxis yAxisId="belly" orientation="right" tick={{fontSize:9,fill:'var(--text3)'}} tickLine={false} domain={['auto','auto']}/>
|
||||
)}
|
||||
<Tooltip contentStyle={{background:'var(--surface)',border:'1px solid var(--border)',borderRadius:8,fontSize:11}}
|
||||
formatter={(v, name) => {
|
||||
if (name === 'vTaper' || name === 'vTaper_avg') return [`${v} cm`, name === 'vTaper_avg' ? 'Ø V-Taper (3 Messungen)' : 'Brust − Taille']
|
||||
if (name === 'belly') return [`${v} cm`, 'Bauch']
|
||||
return [v, name]
|
||||
}}/>
|
||||
<Line yAxisId="taper" type="monotone" dataKey="vTaper" stroke="#1D9E75" strokeWidth={2} dot={{r:3}} name="vTaper"/>
|
||||
<Line yAxisId="taper" type="monotone" dataKey="vTaper_avg" stroke="#1D9E75" strokeWidth={1.5} strokeDasharray="5 4" dot={false} name="vTaper_avg"/>
|
||||
{showBellyOnProp && (
|
||||
<Line yAxisId="belly" type="monotone" dataKey="belly" stroke="#D4537E" strokeWidth={2} dot={{r:3}} connectNulls name="belly"/>
|
||||
)}
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
<div style={{display:'flex',gap:12,justifyContent:'center',marginTop:6,fontSize:10,color:'var(--text3)',flexWrap:'wrap'}}>
|
||||
<span><span style={{display:'inline-block',width:12,height:2,background:'#1D9E75',verticalAlign:'middle',marginRight:3}}/>Brust − Taille</span>
|
||||
<span><span style={{display:'inline-flex',alignItems:'center',gap:3}}><svg width="14" height="4"><line x1="0" y1="2" x2="14" y2="2" stroke="#1D9E75" strokeWidth="2" strokeDasharray="5 4"/></svg></span>gleitender Mittelwert</span>
|
||||
{showBellyOnProp && <span><span style={{display:'inline-block',width:12,height:2,background:'#D4537E',verticalAlign:'middle',marginRight:3}}/>Bauch (cm)</span>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Relative Umfangsänderung (Index erste Messung im Zeitraum = 100) */}
|
||||
{idxOk && (
|
||||
<div className="card" style={{marginBottom:12}}>
|
||||
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:8}}>
|
||||
<div style={{fontSize:12,fontWeight:600,color:'var(--text3)'}}>Umfänge Verlauf</div>
|
||||
<div>
|
||||
<div style={{fontSize:12,fontWeight:600,color:'var(--text3)'}}>Relative Entwicklung der Umfänge</div>
|
||||
<div style={{fontSize:10,color:'var(--text3)',marginTop:4,lineHeight:1.4}}>
|
||||
Index 100 = erste erfasste Messung im Zeitraum. So sind Trend und Richtung besser vergleichbar als absolute cm-Werte nebeneinander.
|
||||
</div>
|
||||
</div>
|
||||
<NavToCircum/>
|
||||
</div>
|
||||
<ResponsiveContainer width="100%" height={180}>
|
||||
<LineChart data={idxSeries} margin={{top:4,right:8,bottom:0,left:-20}}>
|
||||
<CartesianGrid stroke="var(--border)" strokeDasharray="3 3"/>
|
||||
<XAxis dataKey="date" tick={{fontSize:9,fill:'var(--text3)'}} tickLine={false}/>
|
||||
<YAxis tick={{fontSize:9,fill:'var(--text3)'}} tickLine={false} domain={['auto','auto']}/>
|
||||
<ReferenceLine y={100} stroke="var(--text3)" strokeDasharray="4 4" strokeWidth={1}/>
|
||||
<Tooltip contentStyle={{background:'var(--surface)',border:'1px solid var(--border)',borderRadius:8,fontSize:11}}
|
||||
formatter={(v,n)=>[`${v} Index`, n==='chestIdx'?'Brust':n==='waistIdx'?'Taille':'Bauch']}/>
|
||||
{idxSeries.some(d=>d.chestIdx!=null) && <Line type="monotone" dataKey="chestIdx" stroke="#1D9E75" strokeWidth={2} dot={{r:2}} connectNulls name="chestIdx"/>}
|
||||
{idxSeries.some(d=>d.waistIdx!=null) && <Line type="monotone" dataKey="waistIdx" stroke="#EF9F27" strokeWidth={2} dot={{r:2}} connectNulls name="waistIdx"/>}
|
||||
{idxSeries.some(d=>d.bellyIdx!=null) && <Line type="monotone" dataKey="bellyIdx" stroke="#D4537E" strokeWidth={2} dot={{r:2}} connectNulls name="bellyIdx"/>}
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
<div style={{display:'flex',gap:12,justifyContent:'center',marginTop:6,fontSize:10,color:'var(--text3)',flexWrap:'wrap'}}>
|
||||
<span><span style={{display:'inline-block',width:12,height:2,background:'#1D9E75',verticalAlign:'middle',marginRight:3}}/>Brust</span>
|
||||
<span><span style={{display:'inline-block',width:12,height:2,background:'#EF9F27',verticalAlign:'middle',marginRight:3}}/>Taille</span>
|
||||
<span><span style={{display:'inline-block',width:12,height:2,background:'#D4537E',verticalAlign:'middle',marginRight:3}}/>Bauch</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Fallback: klassischer Taille/Hüfte/Bauch-Verlauf wenn keine Brust-Taille-Kombi */}
|
||||
{propChartData.length < 2 && cirCd.length>=2 && (
|
||||
<div className="card" style={{marginBottom:12}}>
|
||||
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:8}}>
|
||||
<div style={{fontSize:12,fontWeight:600,color:'var(--text3)'}}>Umfänge (Taille / Hüfte / Bauch)</div>
|
||||
<NavToCircum/>
|
||||
</div>
|
||||
<div style={{fontSize:10,color:'var(--text3)',marginBottom:8,lineHeight:1.4}}>
|
||||
Sobald Brust- und Taillenumfang gemeinsam erfasst sind, erscheint oben die Proportionen-Ansicht (V-Taper).
|
||||
</div>
|
||||
<ResponsiveContainer width="100%" height={150}>
|
||||
<LineChart data={cirCd} margin={{top:4,right:8,bottom:0,left:-20}}>
|
||||
<CartesianGrid stroke="var(--border)" strokeDasharray="3 3"/>
|
||||
|
|
@ -368,36 +578,7 @@ function BodySection({ weights, calipers, circs, profile, insights, onRequest, l
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* WHR / WHtR detail */}
|
||||
{(whr||whtr) && (
|
||||
<div style={{display:'flex',gap:8,marginBottom:12}}>
|
||||
{whr && <div style={{flex:1,background:'var(--surface2)',borderRadius:8,padding:'10px',textAlign:'center',
|
||||
borderTop:`3px solid ${whr<(sex==='m'?0.90:0.85)?'var(--accent)':'var(--warn)'}`}}>
|
||||
<div style={{fontSize:20,fontWeight:700,color:whr<(sex==='m'?0.90:0.85)?'var(--accent)':'var(--warn)'}}>{whr}</div>
|
||||
<div style={{fontSize:11,fontWeight:600,color:'var(--text3)'}}>WHR</div>
|
||||
<div style={{fontSize:10,color:'var(--text3)'}}>Taille ÷ Hüfte</div>
|
||||
<div style={{fontSize:10,color:'var(--text3)'}}>Ziel <{sex==='m'?'0,90':'0,85'}</div>
|
||||
<div style={{fontSize:10,color:whr<(sex==='m'?0.90:0.85)?'var(--accent)':'var(--warn)'}}>
|
||||
{whr<(sex==='m'?0.90:0.85)?'✓ Günstig':'⚠️ Erhöht'}</div>
|
||||
</div>}
|
||||
{whtr && <div style={{flex:1,background:'var(--surface2)',borderRadius:8,padding:'10px',textAlign:'center',
|
||||
borderTop:`3px solid ${whtr<0.5?'var(--accent)':'var(--warn)'}`}}>
|
||||
<div style={{fontSize:20,fontWeight:700,color:whtr<0.5?'var(--accent)':'var(--warn)'}}>{whtr}</div>
|
||||
<div style={{fontSize:11,fontWeight:600,color:'var(--text3)'}}>WHtR</div>
|
||||
<div style={{fontSize:10,color:'var(--text3)'}}>Taille ÷ Körpergröße</div>
|
||||
<div style={{fontSize:10,color:'var(--text3)'}}>Ziel <0,50</div>
|
||||
<div style={{fontSize:10,color:whtr<0.5?'var(--accent)':'var(--warn)'}}>
|
||||
{whtr<0.5?'✓ Optimal':'⚠️ Erhöht'}</div>
|
||||
</div>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{rules.length>0 && (
|
||||
<div style={{marginBottom:12}}>
|
||||
<div style={{fontSize:12,fontWeight:600,color:'var(--text3)',marginBottom:8}}>BEWERTUNG</div>
|
||||
{rules.map((item,i)=><RuleCard key={i} item={item}/>)}
|
||||
</div>
|
||||
)}
|
||||
<EvaluationTileGrid items={rules} />
|
||||
|
||||
<InsightBox insights={insights} slugs={filterActiveSlugs(['pipeline','koerper','gesundheit','ziele'])}
|
||||
onRequest={onRequest} loading={loadingSlug}/>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user