180 lines
6.1 KiB
TypeScript
180 lines
6.1 KiB
TypeScript
'use client'
|
|
|
|
import { motion } from 'framer-motion'
|
|
import { useState, useEffect } from 'react'
|
|
import { Activity, TrendingUp, Zap, Shield } from 'lucide-react'
|
|
|
|
function AnimatedNumber({ value, suffix = '' }: { value: number; suffix?: string }) {
|
|
const [displayValue, setDisplayValue] = useState(0)
|
|
|
|
useEffect(() => {
|
|
const duration = 2000 // 2 seconds
|
|
const steps = 60
|
|
const increment = value / steps
|
|
const stepDuration = duration / steps
|
|
|
|
let currentStep = 0
|
|
const interval = setInterval(() => {
|
|
currentStep++
|
|
if (currentStep <= steps) {
|
|
setDisplayValue(Math.floor(increment * currentStep))
|
|
} else {
|
|
setDisplayValue(value)
|
|
clearInterval(interval)
|
|
}
|
|
}, stepDuration)
|
|
|
|
return () => clearInterval(interval)
|
|
}, [value])
|
|
|
|
return (
|
|
<span className="font-mono text-2xl lg:text-3xl font-bold text-[hsl(var(--teal))] tabular-nums">
|
|
{displayValue.toLocaleString()}{suffix}
|
|
</span>
|
|
)
|
|
}
|
|
|
|
function FluctuatingNumber({ base, variance }: { base: number; variance: number }) {
|
|
const [value, setValue] = useState(base)
|
|
|
|
useEffect(() => {
|
|
const interval = setInterval(() => {
|
|
const fluctuation = (Math.random() - 0.5) * variance
|
|
setValue(base + fluctuation)
|
|
}, 1500)
|
|
|
|
return () => clearInterval(interval)
|
|
}, [base, variance])
|
|
|
|
return (
|
|
<span className="font-mono text-2xl lg:text-3xl font-bold text-[hsl(var(--teal))] tabular-nums">
|
|
{Math.round(value)}ms
|
|
</span>
|
|
)
|
|
}
|
|
|
|
export function LiveStatsBar() {
|
|
const stats = [
|
|
{
|
|
icon: <Activity className="h-5 w-5" />,
|
|
label: 'Checks performed today',
|
|
value: 2847,
|
|
type: 'counter' as const
|
|
},
|
|
{
|
|
icon: <TrendingUp className="h-5 w-5" />,
|
|
label: 'Changes detected this hour',
|
|
value: 127,
|
|
type: 'counter' as const
|
|
},
|
|
{
|
|
icon: <Shield className="h-5 w-5" />,
|
|
label: 'Uptime',
|
|
value: '99.9%',
|
|
type: 'static' as const
|
|
},
|
|
{
|
|
icon: <Zap className="h-5 w-5" />,
|
|
label: 'Avg response time',
|
|
value: '< ',
|
|
type: 'fluctuating' as const,
|
|
base: 42,
|
|
variance: 10
|
|
}
|
|
]
|
|
|
|
return (
|
|
<section className="border-y border-border bg-gradient-to-r from-foreground/95 via-foreground to-foreground/95 dark:from-secondary dark:via-secondary dark:to-secondary py-8 overflow-hidden">
|
|
<div className="mx-auto max-w-7xl px-6">
|
|
{/* Desktop: Grid */}
|
|
<div className="hidden lg:grid lg:grid-cols-4 gap-8">
|
|
{stats.map((stat, i) => (
|
|
<motion.div
|
|
key={i}
|
|
initial={{ opacity: 0, y: 20 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ delay: i * 0.1, duration: 0.5 }}
|
|
className="flex flex-col items-center text-center gap-3"
|
|
>
|
|
{/* Icon */}
|
|
<motion.div
|
|
className="flex items-center justify-center w-12 h-12 rounded-full bg-[hsl(var(--teal))]/10 text-[hsl(var(--teal))]"
|
|
whileHover={{ scale: 1.1, rotate: 5 }}
|
|
transition={{ duration: 0.2 }}
|
|
>
|
|
{stat.icon}
|
|
</motion.div>
|
|
|
|
{/* Value */}
|
|
<div>
|
|
{stat.type === 'counter' && typeof stat.value === 'number' && (
|
|
<AnimatedNumber value={stat.value} />
|
|
)}
|
|
{stat.type === 'static' && (
|
|
<span className="font-mono text-2xl lg:text-3xl font-bold text-[hsl(var(--teal))]">
|
|
{stat.value}
|
|
</span>
|
|
)}
|
|
{stat.type === 'fluctuating' && stat.base && stat.variance && (
|
|
<span className="font-mono text-2xl lg:text-3xl font-bold text-[hsl(var(--teal))]">
|
|
{stat.value}<FluctuatingNumber base={stat.base} variance={stat.variance} />
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Label */}
|
|
<p className="text-xs font-medium text-white/90 uppercase tracking-wider">
|
|
{stat.label}
|
|
</p>
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Mobile: Horizontal Scroll */}
|
|
<div className="lg:hidden overflow-x-auto scrollbar-thin pb-2">
|
|
<div className="flex gap-8 min-w-max px-4">
|
|
{stats.map((stat, i) => (
|
|
<motion.div
|
|
key={i}
|
|
initial={{ opacity: 0, x: 20 }}
|
|
whileInView={{ opacity: 1, x: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ delay: i * 0.1, duration: 0.5 }}
|
|
className="flex flex-col items-center text-center gap-3 min-w-[160px]"
|
|
>
|
|
{/* Icon */}
|
|
<div className="flex items-center justify-center w-10 h-10 rounded-full bg-[hsl(var(--teal))]/10 text-[hsl(var(--teal))]">
|
|
{stat.icon}
|
|
</div>
|
|
|
|
{/* Value */}
|
|
<div>
|
|
{stat.type === 'counter' && typeof stat.value === 'number' && (
|
|
<AnimatedNumber value={stat.value} />
|
|
)}
|
|
{stat.type === 'static' && (
|
|
<span className="font-mono text-2xl font-bold text-[hsl(var(--teal))]">
|
|
{stat.value}
|
|
</span>
|
|
)}
|
|
{stat.type === 'fluctuating' && stat.base && stat.variance && (
|
|
<span className="font-mono text-2xl font-bold text-[hsl(var(--teal))]">
|
|
{stat.value}<FluctuatingNumber base={stat.base} variance={stat.variance} />
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Label */}
|
|
<p className="text-[10px] font-medium text-white/90 uppercase tracking-wider">
|
|
{stat.label}
|
|
</p>
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|