Initial commit files
This commit is contained in:
79
components/shared/Counter.tsx
Normal file
79
components/shared/Counter.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
interface CounterProps {
|
||||
target: number;
|
||||
suffix?: string;
|
||||
prefix?: string;
|
||||
duration?: number;
|
||||
className?: string;
|
||||
title?: string;
|
||||
numberClassName?: string;
|
||||
titleClassName?: string;
|
||||
layout?: "default" | "row-reverse";
|
||||
gap?: string;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export function Counter({
|
||||
target,
|
||||
suffix = "",
|
||||
prefix = "",
|
||||
duration = 2000,
|
||||
className = "",
|
||||
title,
|
||||
numberClassName = "",
|
||||
titleClassName = "",
|
||||
layout = "default",
|
||||
gap = "0px",
|
||||
style,
|
||||
}: CounterProps) {
|
||||
const [count, setCount] = useState(0);
|
||||
const [hasAnimated, setHasAnimated] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting && !hasAnimated) {
|
||||
setHasAnimated(true);
|
||||
const startTime = performance.now();
|
||||
const animate = (now: number) => {
|
||||
const elapsed = now - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
// ease-out
|
||||
const eased = 1 - Math.pow(1 - progress, 3);
|
||||
setCount(Math.floor(eased * target));
|
||||
if (progress < 1) requestAnimationFrame(animate);
|
||||
};
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
},
|
||||
{ threshold: 0.3 }
|
||||
);
|
||||
|
||||
observer.observe(el);
|
||||
return () => observer.disconnect();
|
||||
}, [target, duration, hasAnimated]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={`${layout === "row-reverse" ? "flex flex-row-reverse items-end" : "flex flex-col"} ${className}`}
|
||||
style={{ gap }}
|
||||
>
|
||||
<div className={`${numberClassName}`} style={style}>
|
||||
{prefix && <span>{prefix}</span>}
|
||||
<span>{count}</span>
|
||||
{suffix && <span>{suffix}</span>}
|
||||
</div>
|
||||
{title && (
|
||||
<div className={`${titleClassName}`}>{title}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user