fix(blog): optimize component share logic, typography, and modal layouts
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
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
This commit is contained in:
69
apps/web/src/components/MetricBar.tsx
Normal file
69
apps/web/src/components/MetricBar.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
'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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user