← back to Norma
components/email-analyzer/Thermometer.tsx
162 lines
'use client';
interface ThermometerProps {
score: number;
prevScore?: number | null;
label?: string;
height?: number;
}
function scoreColor(score: number): string {
if (score >= 95) return '#10b981';
if (score >= 75) return '#10b981';
if (score >= 50) return '#f59e0b';
if (score >= 25) return '#fb923c';
return '#f43f5e';
}
/**
* Live "completion thermometer" — SVG thermometer that fills with mercury
* proportional to the score. Notches mark 25 / 50 / 75 / 100.
*/
export default function Thermometer({ score, prevScore = null, label, height = 180 }: ThermometerProps) {
const clamped = Math.max(0, Math.min(100, score));
const color = scoreColor(clamped);
const bulbRadius = 18;
const tubeWidth = 14;
const tubeX = 30;
const tubeTop = 14;
const tubeBottom = height - bulbRadius - 10;
const tubeHeight = tubeBottom - tubeTop;
const fillHeight = (clamped / 100) * tubeHeight;
const fillY = tubeBottom - fillHeight;
const notches = [0, 25, 50, 75, 100];
const prevY = prevScore !== null ? tubeBottom - (prevScore / 100) * tubeHeight : null;
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<svg width={78} height={height} viewBox={`0 0 78 ${height}`} style={{ flexShrink: 0 }}>
{/* Tube outline */}
<rect
x={tubeX}
y={tubeTop}
width={tubeWidth}
height={tubeHeight}
rx={tubeWidth / 2}
ry={tubeWidth / 2}
fill="var(--color-surface-el)"
stroke="var(--color-border)"
strokeWidth={1.5}
/>
{/* Mercury fill — tube */}
<rect
x={tubeX + 2}
y={fillY}
width={tubeWidth - 4}
height={Math.max(0, tubeBottom - fillY)}
rx={(tubeWidth - 4) / 2}
ry={(tubeWidth - 4) / 2}
fill={color}
style={{ transition: 'y 500ms ease, height 500ms ease, fill 300ms ease' }}
/>
{/* Previous score marker */}
{prevY !== null && Math.abs(prevY - fillY) > 2 && (
<line
x1={tubeX - 4}
x2={tubeX + tubeWidth + 4}
y1={prevY}
y2={prevY}
stroke="var(--color-text-muted)"
strokeWidth={1.5}
strokeDasharray="2,2"
/>
)}
{/* Bulb — background */}
<circle
cx={tubeX + tubeWidth / 2}
cy={tubeBottom + bulbRadius - 2}
r={bulbRadius}
fill={color}
stroke="var(--color-border)"
strokeWidth={1.5}
/>
{/* Bulb highlight */}
<circle
cx={tubeX + tubeWidth / 2 - 4}
cy={tubeBottom + bulbRadius - 5}
r={3}
fill="rgba(255,255,255,0.4)"
/>
{/* Notches */}
{notches.map((n) => {
const y = tubeBottom - (n / 100) * tubeHeight;
return (
<g key={n}>
<line
x1={tubeX + tubeWidth + 2}
x2={tubeX + tubeWidth + 8}
y1={y}
y2={y}
stroke="var(--color-text-muted)"
strokeWidth={1}
/>
<text
x={tubeX + tubeWidth + 11}
y={y + 3}
fontSize={9}
fill="var(--color-text-muted)"
fontFamily="system-ui, sans-serif"
>
{n}
</text>
</g>
);
})}
</svg>
<div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 4 }}>
<span
style={{
fontSize: 30,
fontWeight: 800,
color,
lineHeight: 1,
fontVariantNumeric: 'tabular-nums',
}}
>
{clamped}
</span>
<span style={{ fontSize: 12, color: 'var(--color-text-muted)' }}>/100</span>
</div>
{label && (
<div style={{ fontSize: 11, color: 'var(--color-text-secondary)', fontWeight: 600 }}>
{label}
</div>
)}
<div style={{ fontSize: 10, color: 'var(--color-text-muted)' }}>
{clamped === 100
? '🏆 Maxed out'
: `${100 - clamped} pts to 💯`}
</div>
{prevScore !== null && prevScore !== clamped && (
<div
style={{
fontSize: 10,
color: clamped > prevScore ? 'var(--color-success)' : 'var(--color-error)',
fontWeight: 600,
}}
>
{clamped > prevScore ? '↑' : '↓'} {Math.abs(clamped - prevScore)} from v{prevScore}
</div>
)}
</div>
</div>
);
}