Initial commit from remix
This commit is contained in:
120
src/components/dashboard/CounterCard.tsx
Normal file
120
src/components/dashboard/CounterCard.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import CountUp from 'react-countup';
|
||||
|
||||
interface CounterCardProps {
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
value: number;
|
||||
suffix?: string;
|
||||
trend?: number;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
const CounterCard: React.FC<CounterCardProps> = ({
|
||||
icon,
|
||||
title,
|
||||
value,
|
||||
suffix = '',
|
||||
trend,
|
||||
color = '#F84525'
|
||||
}) => {
|
||||
const cardRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!('IntersectionObserver' in window)) return;
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
(entry.target as Element).classList.add('animate-fade-in-up');
|
||||
}
|
||||
});
|
||||
},
|
||||
{ threshold: 0.1 }
|
||||
);
|
||||
|
||||
try {
|
||||
if (cardRef.current && cardRef.current instanceof Element) {
|
||||
observer.observe(cardRef.current);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('IntersectionObserver.observe failed:', e);
|
||||
}
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={cardRef}
|
||||
className="card-enhanced widget-card position-relative overflow-hidden"
|
||||
style={{ minHeight: '140px' }}
|
||||
>
|
||||
<div className="card-body p-4">
|
||||
<div className="d-flex align-items-center justify-content-between">
|
||||
<div className="flex-grow-1">
|
||||
<div className="d-flex align-items-center mb-2">
|
||||
<div
|
||||
className="rounded-circle p-2 me-3"
|
||||
style={{ backgroundColor: `${color}20`, color: color }}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
<h6 className="text-muted mb-0 small">{title}</h6>
|
||||
</div>
|
||||
|
||||
<div className="d-flex align-items-center">
|
||||
<h3 className="mb-0 fw-bold me-2" style={{ color: color }}>
|
||||
<CountUp
|
||||
start={0}
|
||||
end={value}
|
||||
duration={2.5}
|
||||
separator=","
|
||||
suffix={suffix}
|
||||
/>
|
||||
</h3>
|
||||
|
||||
{trend && (
|
||||
<span
|
||||
className={`badge ${trend > 0 ? 'bg-success' : 'bg-danger'} d-flex align-items-center`}
|
||||
style={{ fontSize: '10px' }}
|
||||
>
|
||||
<i className={`bi bi-arrow-${trend > 0 ? 'up' : 'down'} me-1`}></i>
|
||||
{Math.abs(trend)}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="rounded-circle d-flex align-items-center justify-content-center"
|
||||
style={{
|
||||
width: '60px',
|
||||
height: '60px',
|
||||
backgroundColor: `${color}10`,
|
||||
color: color
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Animated background element */}
|
||||
<div
|
||||
className="position-absolute"
|
||||
style={{
|
||||
bottom: '-20px',
|
||||
right: '-20px',
|
||||
width: '80px',
|
||||
height: '80px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: `${color}08`,
|
||||
opacity: 0.3
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CounterCard;
|
||||
Reference in New Issue
Block a user