Added animated stats from 0 to their actual value.
This commit is contained in:
parent
36f509f6bc
commit
aacf530b81
|
|
@ -0,0 +1,58 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { useInView, useSpring, useTransform, motion } from "framer-motion";
|
||||||
|
|
||||||
|
interface AnimatedCounterProps {
|
||||||
|
value: number;
|
||||||
|
suffix?: string;
|
||||||
|
prefix?: string;
|
||||||
|
duration?: number;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AnimatedCounter({
|
||||||
|
value,
|
||||||
|
suffix = "",
|
||||||
|
prefix = "",
|
||||||
|
duration = 2,
|
||||||
|
className = "",
|
||||||
|
}: AnimatedCounterProps) {
|
||||||
|
const ref = useRef<HTMLSpanElement>(null);
|
||||||
|
const isInView = useInView(ref, { once: true, margin: "-50px" });
|
||||||
|
const [hasAnimated, setHasAnimated] = useState(false);
|
||||||
|
|
||||||
|
const spring = useSpring(0, {
|
||||||
|
stiffness: 50,
|
||||||
|
damping: 30,
|
||||||
|
duration: duration * 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const display = useTransform(spring, (current) => {
|
||||||
|
return Math.round(current);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [displayValue, setDisplayValue] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isInView && !hasAnimated) {
|
||||||
|
setHasAnimated(true);
|
||||||
|
spring.set(value);
|
||||||
|
}
|
||||||
|
}, [isInView, hasAnimated, spring, value]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribe = display.on("change", (v) => {
|
||||||
|
setDisplayValue(v);
|
||||||
|
});
|
||||||
|
return unsubscribe;
|
||||||
|
}, [display]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.span ref={ref} className={className}>
|
||||||
|
{prefix}
|
||||||
|
{displayValue}
|
||||||
|
{suffix}
|
||||||
|
</motion.span>
|
||||||
|
);
|
||||||
|
}
|
||||||
30
app/page.tsx
30
app/page.tsx
|
|
@ -20,12 +20,13 @@ import {
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
RecycleIcon,
|
RecycleIcon,
|
||||||
} from "./components/Icons";
|
} from "./components/Icons";
|
||||||
|
import AnimatedCounter from "./components/AnimatedCounter";
|
||||||
|
|
||||||
const stats = [
|
const stats = [
|
||||||
{ value: "100+", label: "Years Combined Experience" },
|
{ value: 100, suffix: "+", label: "Years Combined Experience" },
|
||||||
{ value: "10", label: "States Covered" },
|
{ value: 10, suffix: "", label: "States Covered" },
|
||||||
{ value: "2005", label: "Industry Trusted Since" },
|
{ value: 2005, suffix: "", label: "Industry Trusted Since" },
|
||||||
{ value: "5", label: "Product Categories" },
|
{ value: 5, suffix: "", label: "Product Categories" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const productCategories = [
|
const productCategories = [
|
||||||
|
|
@ -271,20 +272,13 @@ export default function Home() {
|
||||||
whileHover={{ scale: 1.05 }}
|
whileHover={{ scale: 1.05 }}
|
||||||
className="group cursor-default"
|
className="group cursor-default"
|
||||||
>
|
>
|
||||||
<motion.span
|
<div className="block text-4xl sm:text-5xl font-bold text-white mb-2 group-hover:text-[hsl(0,100%,60%)] transition-colors">
|
||||||
className="block text-4xl sm:text-5xl font-bold text-white mb-2 group-hover:text-[hsl(0,100%,60%)] transition-colors"
|
<AnimatedCounter
|
||||||
initial={{ opacity: 0, scale: 0.5 }}
|
value={stat.value}
|
||||||
whileInView={{ opacity: 1, scale: 1 }}
|
suffix={stat.suffix}
|
||||||
viewport={{ once: true }}
|
duration={2}
|
||||||
transition={{
|
/>
|
||||||
type: "spring",
|
</div>
|
||||||
stiffness: 200,
|
|
||||||
damping: 15,
|
|
||||||
delay: index * 0.1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{stat.value}
|
|
||||||
</motion.span>
|
|
||||||
<span className="text-neutral-400 text-sm sm:text-base">
|
<span className="text-neutral-400 text-sm sm:text-base">
|
||||||
{stat.label}
|
{stat.label}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue