2025-07-10 18:04:58 +02:00
|
|
|
import { cn } from '@/lib/utils';
|
|
|
|
|
import { ChevronLeft, ChevronRight, Plus } from 'lucide-react';
|
|
|
|
|
import { useEffect, useState } from 'react';
|
|
|
|
|
|
|
|
|
|
interface LedCounterProps {
|
|
|
|
|
value: number;
|
|
|
|
|
className?: string;
|
|
|
|
|
animate?: boolean;
|
|
|
|
|
currentPrice?: number;
|
|
|
|
|
onHover?: (isHovered: boolean) => void;
|
|
|
|
|
// Progress bar props
|
|
|
|
|
onStatsToggle?: () => void;
|
|
|
|
|
showStats?: boolean;
|
|
|
|
|
onAddPurchase?: () => void;
|
2025-07-12 18:09:11 +02:00
|
|
|
onAddMilestone?: () => void;
|
2025-07-10 18:04:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function LedCounter({
|
|
|
|
|
value,
|
|
|
|
|
className,
|
|
|
|
|
animate = true,
|
|
|
|
|
currentPrice,
|
|
|
|
|
onHover,
|
|
|
|
|
onStatsToggle,
|
|
|
|
|
showStats = false,
|
2025-07-12 18:09:11 +02:00
|
|
|
onAddPurchase,
|
|
|
|
|
onAddMilestone
|
2025-07-10 18:04:58 +02:00
|
|
|
}: LedCounterProps) {
|
|
|
|
|
const [displayValue, setDisplayValue] = useState(0);
|
|
|
|
|
const [isHovered, setIsHovered] = useState(false);
|
|
|
|
|
const [hoverTimeout, setHoverTimeout] = useState<NodeJS.Timeout | null>(null);
|
|
|
|
|
const [currentMilestoneIndex, setCurrentMilestoneIndex] = useState(0);
|
|
|
|
|
|
|
|
|
|
// Milestone definitions
|
|
|
|
|
const milestones = [
|
|
|
|
|
{ target: 1500, label: '1.5K', color: 'bg-blue-500' },
|
|
|
|
|
{ target: 3000, label: '3K', color: 'bg-green-500' },
|
|
|
|
|
{ target: 4500, label: '4.5K', color: 'bg-yellow-500' },
|
|
|
|
|
{ target: 6000, label: '6K', color: 'bg-red-500' },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const currentMilestone = milestones[currentMilestoneIndex];
|
|
|
|
|
const progress = Math.min((value / currentMilestone.target) * 100, 100);
|
|
|
|
|
const isCompleted = value >= currentMilestone.target;
|
|
|
|
|
|
|
|
|
|
// Milestone navigation
|
|
|
|
|
const nextMilestone = () => {
|
|
|
|
|
setCurrentMilestoneIndex((prev) =>
|
|
|
|
|
prev < milestones.length - 1 ? prev + 1 : 0
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const prevMilestone = () => {
|
|
|
|
|
setCurrentMilestoneIndex((prev) =>
|
|
|
|
|
prev > 0 ? prev - 1 : milestones.length - 1
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleProgressBarClick = () => {
|
|
|
|
|
if (onStatsToggle) {
|
|
|
|
|
onStatsToggle();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
|
|
|
|
const formatCurrency = (amount: number) => {
|
|
|
|
|
return new Intl.NumberFormat('de-DE', {
|
|
|
|
|
style: 'currency',
|
|
|
|
|
currency: 'EUR',
|
|
|
|
|
minimumFractionDigits: 4,
|
|
|
|
|
}).format(amount);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
className={cn(
|
|
|
|
|
"font-mono text-center select-none relative group 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
|
|
|
|
|
)}
|
|
|
|
|
onMouseEnter={() => {
|
|
|
|
|
if (hoverTimeout) {
|
|
|
|
|
clearTimeout(hoverTimeout);
|
|
|
|
|
setHoverTimeout(null);
|
|
|
|
|
}
|
|
|
|
|
setIsHovered(true);
|
|
|
|
|
if (onHover) onHover(true);
|
|
|
|
|
}}
|
|
|
|
|
onMouseLeave={() => {
|
|
|
|
|
// Delay hiding to allow moving to progress bar
|
|
|
|
|
const timeout = setTimeout(() => {
|
|
|
|
|
setIsHovered(false);
|
|
|
|
|
if (onHover) onHover(false);
|
|
|
|
|
}, 300); // 300ms delay
|
|
|
|
|
setHoverTimeout(timeout);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<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>
|
|
|
|
|
|
|
|
|
|
{/* Hover overlay with price and add button */}
|
|
|
|
|
<div className={cn(
|
|
|
|
|
"absolute inset-0 bg-black/90 backdrop-blur-sm",
|
|
|
|
|
"flex flex-col items-center justify-center",
|
|
|
|
|
"transition-all duration-300",
|
|
|
|
|
"rounded-lg z-50",
|
|
|
|
|
isHovered ? "opacity-100" : "opacity-0 pointer-events-none"
|
|
|
|
|
)}>
|
|
|
|
|
{/* Current Price Display */}
|
|
|
|
|
{currentPrice ? (
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
<div className="text-red-400/70 text-sm font-medium tracking-wide mb-2">
|
|
|
|
|
current price
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-red-500 text-4xl md:text-6xl font-mono-display tracking-wider">
|
|
|
|
|
{formatCurrency(currentPrice)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="text-red-400/50 text-sm font-medium">
|
|
|
|
|
no price data
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Progress Bar - shows when hovered */}
|
|
|
|
|
<div className={cn(
|
|
|
|
|
"absolute bottom-0 left-0 right-0",
|
|
|
|
|
"bg-black/90 backdrop-blur-sm",
|
|
|
|
|
"border-t border-red-500/30",
|
|
|
|
|
"transition-all duration-300 transform",
|
|
|
|
|
"z-[60]", // Higher than hover overlay z-50
|
|
|
|
|
isHovered ? "translate-y-0 opacity-100" : "translate-y-full opacity-0",
|
|
|
|
|
)}>
|
|
|
|
|
<div className="relative">
|
|
|
|
|
{/* Progress Bar */}
|
|
|
|
|
<div
|
|
|
|
|
className="h-2 bg-gray-800 cursor-pointer relative overflow-hidden"
|
|
|
|
|
onClick={handleProgressBarClick}
|
|
|
|
|
>
|
|
|
|
|
{/* Background pulse for completed milestones */}
|
|
|
|
|
{isCompleted && (
|
|
|
|
|
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent animate-pulse" />
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Progress fill */}
|
|
|
|
|
<div
|
|
|
|
|
className={cn(
|
|
|
|
|
"h-full transition-all duration-1000 ease-out",
|
|
|
|
|
currentMilestone.color,
|
|
|
|
|
"shadow-lg",
|
|
|
|
|
isCompleted ? "animate-pulse" : ""
|
|
|
|
|
)}
|
|
|
|
|
style={{ width: `${progress}%` }}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{/* Glow effect */}
|
|
|
|
|
<div
|
|
|
|
|
className={cn(
|
|
|
|
|
"absolute top-0 h-full transition-all duration-1000 ease-out",
|
|
|
|
|
"bg-gradient-to-r from-transparent to-white/30",
|
|
|
|
|
"blur-sm"
|
|
|
|
|
)}
|
|
|
|
|
style={{ width: `${Math.min(progress + 10, 100)}%` }}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Milestone Info */}
|
|
|
|
|
<div className="flex items-center justify-between px-4 py-2">
|
|
|
|
|
{/* Left: Previous milestone button */}
|
|
|
|
|
<button
|
|
|
|
|
onClick={prevMilestone}
|
|
|
|
|
className="text-red-400 hover:text-red-300 transition-colors p-1"
|
|
|
|
|
aria-label="Previous milestone"
|
|
|
|
|
>
|
|
|
|
|
<ChevronLeft className="w-4 h-4" />
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
{/* Center: Milestone info */}
|
|
|
|
|
<div className="flex items-center space-x-4 text-center">
|
|
|
|
|
<div className="text-red-400 text-sm font-mono">
|
|
|
|
|
{value.toFixed(2)} / {currentMilestone.target}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="text-red-500 font-bold text-sm">
|
|
|
|
|
{currentMilestone.label}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="text-red-400 text-sm">
|
|
|
|
|
{isCompleted ? 'COMPLETED' : `${(100 - progress).toFixed(1)}% TO GO`}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-07-12 18:09:11 +02:00
|
|
|
{/* Right: Add Purchase, Add Milestone, Next milestone button and stats toggle */}
|
2025-07-10 18:04:58 +02:00
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
{/* Add Purchase Button */}
|
|
|
|
|
{onAddPurchase && (
|
|
|
|
|
<button
|
|
|
|
|
onClick={onAddPurchase}
|
|
|
|
|
className="flex items-center space-x-1 px-2 py-1 rounded bg-red-600/20 border border-red-500/50 text-red-400 hover:bg-red-600/40 hover:text-red-300 transition-colors text-xs"
|
|
|
|
|
aria-label="Add purchase"
|
|
|
|
|
>
|
|
|
|
|
<Plus className="w-3 h-3" />
|
|
|
|
|
<span className="font-mono">ADD</span>
|
2025-07-12 18:09:11 +02:00
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Add Milestone Button */}
|
|
|
|
|
{onAddMilestone && (
|
|
|
|
|
<button
|
|
|
|
|
onClick={onAddMilestone}
|
|
|
|
|
className="flex items-center space-x-1 px-2 py-1 rounded bg-blue-600/20 border border-blue-500/50 text-blue-400 hover:bg-blue-600/40 hover:text-blue-300 transition-colors text-xs"
|
|
|
|
|
aria-label="Add milestone"
|
|
|
|
|
>
|
|
|
|
|
<Plus className="w-3 h-3" />
|
|
|
|
|
<span className="font-mono">MILE</span>
|
2025-07-10 18:04:58 +02:00
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
onClick={nextMilestone}
|
|
|
|
|
className="text-red-400 hover:text-red-300 transition-colors p-1"
|
|
|
|
|
aria-label="Next milestone"
|
|
|
|
|
>
|
|
|
|
|
<ChevronRight className="w-4 h-4" />
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
{/* Stats indicator */}
|
|
|
|
|
<div className={cn(
|
|
|
|
|
"text-xs text-red-400/60 cursor-pointer transition-colors",
|
|
|
|
|
showStats && "text-red-400"
|
|
|
|
|
)}>
|
|
|
|
|
{showStats ? '▲' : '▼'}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|