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
70 lines
2.0 KiB
TypeScript
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>
|
|
);
|
|
};
|