Initial Next.js clone of Pars Elevator (plift.ir).
RTL Meshkee storefront with home/about/contact/products/blog, API auth/contact/blog wiring, and SEO sitemap-config. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
type Props = {
|
||||
value: number;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function Counter({ value, className = "" }: Props) {
|
||||
const [n, setN] = useState(0);
|
||||
const ref = useRef<HTMLSpanElement>(null);
|
||||
const started = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el || started.current) return;
|
||||
|
||||
const run = () => {
|
||||
if (started.current) return;
|
||||
started.current = true;
|
||||
const duration = 1400;
|
||||
const start = performance.now();
|
||||
const tick = (now: number) => {
|
||||
const t = Math.min(1, (now - start) / duration);
|
||||
const eased = 1 - Math.pow(1 - t, 3);
|
||||
setN(Math.round(value * eased));
|
||||
if (t < 1) requestAnimationFrame(tick);
|
||||
else setN(value);
|
||||
};
|
||||
requestAnimationFrame(tick);
|
||||
};
|
||||
|
||||
const isVisible = () => {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const vh = window.innerHeight || document.documentElement.clientHeight;
|
||||
return rect.top < vh * 0.92 && rect.bottom > vh * 0.08;
|
||||
};
|
||||
|
||||
if (isVisible()) {
|
||||
run();
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
run();
|
||||
observer.disconnect();
|
||||
}
|
||||
},
|
||||
{ threshold: 0.2 },
|
||||
);
|
||||
|
||||
observer.observe(el);
|
||||
return () => observer.disconnect();
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<span ref={ref} className={className} dir="ltr">
|
||||
{n.toLocaleString("en-US")}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user