Files
mintel.me/apps/web/src/components/MetricBar.tsx
Marc Mintel b15c8408ff
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 5s
Build & Deploy / 🏗️ Build (push) Failing after 14s
Build & Deploy / 🧪 QA (push) Failing after 1m48s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
fix(blog): optimize component share logic, typography, and modal layouts
2026-02-22 11:41:28 +01:00

70 lines
2.0 KiB
TypeScript

'use client';
import React, { useEffect, useRef, useState } from 'react';
interface MetricBarProps {
label: string;
value: number;
max?: number;
unit?: string;
/** "red" | "green" | "blue" | "slate" */
color?: 'red' | 'green' | 'blue' | 'slate';
className?: string;
}
const colorMap = {
red: 'bg-red-400',
green: 'bg-emerald-500',
blue: 'bg-blue-500',
slate: 'bg-slate-700',
};
export const MetricBar: React.FC<MetricBarProps> = ({
label,
value,
max = 100,
unit = '%',
color = 'slate',
className = '',
}) => {
const [animated, setAnimated] = useState(false);
const ref = useRef<HTMLDivElement>(null);
const pct = Math.min(100, Math.round((value / max) * 100));
useEffect(() => {
if (!ref.current) return;
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setAnimated(true);
observer.disconnect();
}
});
},
{ rootMargin: '50px' }
);
observer.observe(ref.current);
return () => observer.disconnect();
}, []);
return (
<div ref={ref} className={`not-prose my-3 ${className}`}>
<div className="flex items-center justify-between mb-1.5">
<span className="text-sm font-semibold text-slate-700">{label}</span>
<span className="text-sm font-bold text-slate-900 tabular-nums">
{value}{unit}
</span>
</div>
<div className="h-3 bg-slate-100 rounded-full overflow-hidden border border-slate-200">
<div
className={`h-full rounded-full transition-all duration-1000 ease-out ${colorMap[color]}`}
style={{ width: animated ? `${pct}%` : '0%' }}
/>
</div>
</div>
);
};