gitea
This commit is contained in:
179
frontend/components/landing/LiveStatsBar.tsx
Normal file
179
frontend/components/landing/LiveStatsBar.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
'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 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user