mitai-jinkendo/frontend/src/components/DashboardStatKit.jsx

135 lines
4.3 KiB
React
Raw Normal View History

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>
)
}