104 lines
3.3 KiB
TypeScript
104 lines
3.3 KiB
TypeScript
|
|
import { cn } from '@/lib/utils';
|
||
|
|
import { useEffect, useState } from 'react';
|
||
|
|
|
||
|
|
interface LedDisplayProps {
|
||
|
|
value: number;
|
||
|
|
className?: string;
|
||
|
|
animate?: boolean;
|
||
|
|
onClick?: () => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
export default function LedDisplay({
|
||
|
|
value,
|
||
|
|
className,
|
||
|
|
animate = true,
|
||
|
|
onClick
|
||
|
|
}: LedDisplayProps) {
|
||
|
|
const [displayValue, setDisplayValue] = useState(0);
|
||
|
|
|
||
|
|
// Animate number changes
|
||
|
|
useEffect(() => {
|
||
|
|
if (!animate) {
|
||
|
|
setDisplayValue(value);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const duration = 1000; // 1 second animation
|
||
|
|
const steps = 60; // 60fps
|
||
|
|
const stepValue = (value - displayValue) / steps;
|
||
|
|
|
||
|
|
if (Math.abs(stepValue) < 0.01) {
|
||
|
|
setDisplayValue(value);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const timer = setInterval(() => {
|
||
|
|
setDisplayValue(prev => {
|
||
|
|
const next = prev + stepValue;
|
||
|
|
if (Math.abs(next - value) < Math.abs(stepValue)) {
|
||
|
|
clearInterval(timer);
|
||
|
|
return value;
|
||
|
|
}
|
||
|
|
return next;
|
||
|
|
});
|
||
|
|
}, duration / steps);
|
||
|
|
|
||
|
|
return () => clearInterval(timer);
|
||
|
|
}, [value, displayValue, animate]);
|
||
|
|
|
||
|
|
// Format number appropriately for shares
|
||
|
|
const formatValue = (value: number) => {
|
||
|
|
// If it's a whole number, show it as integer
|
||
|
|
if (value % 1 === 0) {
|
||
|
|
return value.toString();
|
||
|
|
}
|
||
|
|
// Otherwise show up to 6 decimal places, removing trailing zeros
|
||
|
|
return value.toFixed(6).replace(/\.?0+$/, '');
|
||
|
|
};
|
||
|
|
|
||
|
|
const formattedValue = formatValue(displayValue);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div
|
||
|
|
className={cn(
|
||
|
|
"font-mono text-center select-none cursor-pointer",
|
||
|
|
"bg-black text-red-500",
|
||
|
|
"border-4 border-gray-800 rounded-lg",
|
||
|
|
"shadow-2xl shadow-red-500/20",
|
||
|
|
"p-8 transition-all duration-300",
|
||
|
|
"hover:shadow-red-500/40 hover:border-red-600",
|
||
|
|
className
|
||
|
|
)}
|
||
|
|
onClick={onClick}
|
||
|
|
>
|
||
|
|
<div className="relative">
|
||
|
|
{/* Background glow effect */}
|
||
|
|
<div className="absolute inset-0 text-red-500/20 blur-sm font-mono-display text-6xl md:text-8xl lg:text-9xl tracking-widest">
|
||
|
|
{formattedValue}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Main LED text */}
|
||
|
|
<div className={cn(
|
||
|
|
"relative z-10",
|
||
|
|
"text-6xl md:text-8xl lg:text-9xl",
|
||
|
|
"font-mono-display font-normal tracking-widest",
|
||
|
|
"text-red-500",
|
||
|
|
"drop-shadow-[0_0_10px_rgba(239,68,68,0.8)]",
|
||
|
|
"filter brightness-110"
|
||
|
|
)}>
|
||
|
|
{formattedValue}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Subtle scan line effect */}
|
||
|
|
<div className="absolute inset-0 pointer-events-none">
|
||
|
|
<div className="h-full w-full bg-gradient-to-b from-transparent via-red-500/5 to-transparent animate-pulse" />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Label */}
|
||
|
|
<div className="mt-4 text-red-400/80 text-sm md:text-base font-mono-display tracking-wider">
|
||
|
|
total shares
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|