Import source: src/components/StatsBar
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
@@ -0,0 +1,78 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect, useRef } from 'react';
|
||||||
|
import { useInView } from 'framer-motion';
|
||||||
|
import { useLocale, useTranslations } from 'next-intl';
|
||||||
|
|
||||||
|
type CountUpProps = {
|
||||||
|
end: number;
|
||||||
|
duration?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function CountUp({ end, duration = 1800 }: CountUpProps) {
|
||||||
|
const [count, setCount] = useState(0);
|
||||||
|
const ref = useRef(null);
|
||||||
|
const isInView = useInView(ref, { once: true, margin: '-50px' });
|
||||||
|
const locale = useLocale();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isInView) return;
|
||||||
|
|
||||||
|
let startTime: number | null = null;
|
||||||
|
const startValue = 0;
|
||||||
|
|
||||||
|
const animate = (timestamp: number) => {
|
||||||
|
if (!startTime) startTime = timestamp;
|
||||||
|
const elapsed = timestamp - startTime;
|
||||||
|
const progress = Math.min(elapsed / duration, 1);
|
||||||
|
|
||||||
|
// Easing function: easeOutQuad
|
||||||
|
const easeProgress = progress * (2 - progress);
|
||||||
|
|
||||||
|
const current = Math.floor(easeProgress * (end - startValue) + startValue);
|
||||||
|
setCount(current);
|
||||||
|
|
||||||
|
if (progress < 1) {
|
||||||
|
requestAnimationFrame(animate);
|
||||||
|
} else {
|
||||||
|
setCount(end);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
requestAnimationFrame(animate);
|
||||||
|
}, [isInView, end, duration]);
|
||||||
|
|
||||||
|
return <span ref={ref}>{count.toLocaleString(locale === 'fa' ? 'fa-IR' : 'en-US')}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function StatsBar() {
|
||||||
|
const t = useTranslations('Stats');
|
||||||
|
|
||||||
|
const stats = [
|
||||||
|
{ value: 28, key: 'articles' },
|
||||||
|
{ value: 118280, key: 'views' },
|
||||||
|
{ value: 4789, key: 'reactions' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="w-full bg-sand/20 dark:bg-charcoal/20 border-y border-sand/40 dark:border-charcoal/40 py-8 transition-colors duration-300">
|
||||||
|
<div className="max-w-[1200px] mx-auto px-6 grid grid-cols-1 md:grid-cols-3 gap-8 text-center">
|
||||||
|
{stats.map((stat, idx) => (
|
||||||
|
<div
|
||||||
|
key={stat.key}
|
||||||
|
className={`flex flex-col items-center justify-center ${
|
||||||
|
idx !== stats.length - 1 ? 'md:border-r border-sand/50 dark:border-charcoal/50 rtl:md:border-r-0 rtl:md:border-l' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="text-3xl md:text-5xl font-black text-teal-brand dark:text-teal-brand tracking-tight mb-2">
|
||||||
|
<CountUp end={stat.value} />
|
||||||
|
</div>
|
||||||
|
<div className="text-xs uppercase tracking-widest text-charcoal/60 dark:text-warm-white/50 font-semibold">
|
||||||
|
{t(stat.key)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user