135 lines
4.3 KiB
React
135 lines
4.3 KiB
React
|
|
import { useState } from 'react'
|
|||
|
|
import {
|
|||
|
|
clampTileSpan,
|
|||
|
|
DASHBOARD_TILE_GRID_COLS,
|
|||
|
|
} from '../utils/dashboardLayout'
|
|||
|
|
|
|||
|
|
export const PILL_TOOLTIPS = {
|
|||
|
|
WHR: 'Waist-Hip-Ratio: Taille ÷ Hüfte. Maß für Bauchfettverteilung. Ziel: <0,90 (M) / <0,85 (F)',
|
|||
|
|
WHtR: 'Waist-to-Height-Ratio: Taille ÷ Körpergröße. Gesündestest Maß: Ziel unter 0,50.',
|
|||
|
|
KF: 'Körperfettanteil in Prozent (aus Caliper-Messung).',
|
|||
|
|
'Protein Ø7T':
|
|||
|
|
'Durchschnittliche tägliche Proteinaufnahme der letzten 7 Tage vs. Zielbereich (1,6–2,2g/kg KG).',
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function Pill({ label, value, status, sub }) {
|
|||
|
|
const [tip, setTip] = useState(false)
|
|||
|
|
const color = status === 'good' ? 'var(--accent)' : status === 'warn' ? 'var(--warn)' : '#D85A30'
|
|||
|
|
const bg =
|
|||
|
|
status === 'good'
|
|||
|
|
? 'var(--accent-light)'
|
|||
|
|
: status === 'warn'
|
|||
|
|
? 'var(--warn-bg)'
|
|||
|
|
: '#FCEBEB'
|
|||
|
|
const tipText = PILL_TOOLTIPS[label]
|
|||
|
|
return (
|
|||
|
|
<div style={{ position: 'relative' }}>
|
|||
|
|
<div
|
|||
|
|
role={tipText ? 'button' : undefined}
|
|||
|
|
onClick={() => tipText && setTip((s) => !s)}
|
|||
|
|
onKeyDown={(e) => tipText && e.key === 'Enter' && setTip((s) => !s)}
|
|||
|
|
tabIndex={tipText ? 0 : undefined}
|
|||
|
|
style={{
|
|||
|
|
display: 'flex',
|
|||
|
|
alignItems: 'center',
|
|||
|
|
gap: 5,
|
|||
|
|
padding: '5px 10px',
|
|||
|
|
borderRadius: 20,
|
|||
|
|
background: bg,
|
|||
|
|
border: `1px solid ${color}44`,
|
|||
|
|
cursor: tipText ? 'help' : 'default',
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<div style={{ width: 7, height: 7, borderRadius: '50%', background: color, flexShrink: 0 }} />
|
|||
|
|
<span style={{ fontSize: 12, fontWeight: 500, color: 'var(--text2)' }}>{label}</span>
|
|||
|
|
<span style={{ fontSize: 12, fontWeight: 700, color }}>{value}</span>
|
|||
|
|
{sub && <span style={{ fontSize: 10, color: 'var(--text3)' }}>{sub}</span>}
|
|||
|
|
{tipText && (
|
|||
|
|
<span style={{ fontSize: 10, color: 'var(--text3)', opacity: 0.7 }} aria-hidden>
|
|||
|
|
ⓘ
|
|||
|
|
</span>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
{tip && tipText && (
|
|||
|
|
<div
|
|||
|
|
role="tooltip"
|
|||
|
|
onClick={() => setTip(false)}
|
|||
|
|
style={{
|
|||
|
|
position: 'absolute',
|
|||
|
|
bottom: '110%',
|
|||
|
|
left: 0,
|
|||
|
|
zIndex: 50,
|
|||
|
|
background: 'var(--surface)',
|
|||
|
|
border: '1px solid var(--border)',
|
|||
|
|
borderRadius: 8,
|
|||
|
|
padding: '8px 10px',
|
|||
|
|
fontSize: 11,
|
|||
|
|
color: 'var(--text2)',
|
|||
|
|
minWidth: 200,
|
|||
|
|
maxWidth: 260,
|
|||
|
|
lineHeight: 1.5,
|
|||
|
|
boxShadow: '0 4px 16px rgba(0,0,0,0.15)',
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<strong>{label}</strong>
|
|||
|
|
<br />
|
|||
|
|
{tipText}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* KPI-Kachel (Dashboard-Raster).
|
|||
|
|
*/
|
|||
|
|
export 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',
|
|||
|
|
'--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>
|
|||
|
|
<div style={{ fontSize: 11, color: 'var(--text3)', marginBottom: 2 }}>{label}</div>
|
|||
|
|
<div style={{ fontSize: 19, fontWeight: 700, color: color || 'var(--text1)', lineHeight: 1.1 }}>
|
|||
|
|
{value}
|
|||
|
|
<span style={{ fontSize: 12, fontWeight: 400, color: 'var(--text3)', marginLeft: 2 }}>{unit}</span>
|
|||
|
|
</div>
|
|||
|
|
{delta != null && (
|
|||
|
|
<div style={{ fontSize: 11, fontWeight: 600, color: deltaColor, marginTop: 2 }}>
|
|||
|
|
{delta > 0 ? '+' : ''}
|
|||
|
|
{delta} {unit}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
{sub && <div style={{ fontSize: 10, color: 'var(--text3)', marginTop: 2 }}>{sub}</div>}
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
}
|