This commit is contained in:
NotII
2025-03-28 22:56:57 +00:00
parent 546e7bd245
commit 1bec653206
5 changed files with 274 additions and 24 deletions

View File

@@ -0,0 +1,71 @@
"use client";
import { useState, useEffect, useRef } from "react";
export interface AnimatedCounterProps {
value: number;
duration?: number;
prefix?: string;
suffix?: string;
formatter?: (value: number) => string;
}
export function AnimatedCounter({
value,
duration = 2000,
prefix = "",
suffix = "",
formatter
}: AnimatedCounterProps) {
const [displayValue, setDisplayValue] = useState(0);
const startTimeRef = useRef<number | null>(null);
const frameRef = useRef<number | null>(null);
// Easing function for smoother animation
const easeOutQuad = (t: number): number => t * (2 - t);
useEffect(() => {
// Reset start time on new value
startTimeRef.current = null;
// Cancel any ongoing animation
if (frameRef.current) {
cancelAnimationFrame(frameRef.current);
}
// Animation function
const animate = (timestamp: number) => {
if (!startTimeRef.current) {
startTimeRef.current = timestamp;
}
const elapsed = timestamp - startTimeRef.current;
const progress = Math.min(elapsed / duration, 1);
const easedProgress = easeOutQuad(progress);
const nextValue = Math.floor(easedProgress * value);
setDisplayValue(nextValue);
if (progress < 1) {
frameRef.current = requestAnimationFrame(animate);
} else {
setDisplayValue(value);
}
};
// Start the animation
frameRef.current = requestAnimationFrame(animate);
return () => {
if (frameRef.current) {
cancelAnimationFrame(frameRef.current);
}
};
}, [value, duration]);
const formattedValue = formatter
? formatter(displayValue)
: `${prefix}${displayValue.toLocaleString()}${suffix}`;
return <span>{formattedValue}</span>;
}

View File

@@ -0,0 +1,105 @@
"use client";
import { Package, Users, CreditCard } from "lucide-react";
import { AnimatedCounter } from "./animated-counter";
import { TextTyper } from "./text-typer";
import { PlatformStats } from "@/lib/stats-service";
const formatCurrency = (value: number) => {
return new Intl.NumberFormat('en-GB', {
style: 'currency',
currency: 'GBP',
minimumFractionDigits: 0,
}).format(value);
};
interface AnimatedStatsProps {
stats: PlatformStats;
}
export function AnimatedStatsSection({ stats }: AnimatedStatsProps) {
return (
<div className="mx-auto grid max-w-5xl grid-cols-1 gap-8 md:grid-cols-3 mt-8">
<div className="flex flex-col items-center space-y-2 rounded-lg border border-zinc-800 p-6 transition-all hover:bg-zinc-800">
<div className="rounded-full bg-zinc-900 p-3 text-[#D53F8C]">
<Package className="h-8 w-8" />
</div>
<div className="text-3xl font-bold text-white">
<AnimatedCounter
value={stats.orders.completed}
duration={2000}
suffix="+"
/>
</div>
<h3 className="text-xl font-bold text-white">
<TextTyper
text="Completed Orders"
typingSpeed={30}
delay={500}
/>
</h3>
<p className="text-center text-zinc-400">
<TextTyper
text="Successfully fulfilled orders across our platform"
typingSpeed={10}
delay={1500}
/>
</p>
</div>
<div className="flex flex-col items-center space-y-2 rounded-lg border border-zinc-800 p-6 transition-all hover:bg-zinc-800">
<div className="rounded-full bg-zinc-900 p-3 text-[#D53F8C]">
<Users className="h-8 w-8" />
</div>
<div className="text-3xl font-bold text-white">
<AnimatedCounter
value={stats.vendors.total}
duration={2000}
suffix="+"
/>
</div>
<h3 className="text-xl font-bold text-white">
<TextTyper
text="Registered Vendors"
typingSpeed={30}
delay={1000}
/>
</h3>
<p className="text-center text-zinc-400">
<TextTyper
text="Active sellers offering products on our marketplace"
typingSpeed={10}
delay={2000}
/>
</p>
</div>
<div className="flex flex-col items-center space-y-2 rounded-lg border border-zinc-800 p-6 transition-all hover:bg-zinc-800">
<div className="rounded-full bg-zinc-900 p-3 text-[#D53F8C]">
<CreditCard className="h-8 w-8" />
</div>
<div className="text-3xl font-bold text-white">
<AnimatedCounter
value={stats.transactions.volume}
duration={2500}
formatter={formatCurrency}
/>
</div>
<h3 className="text-xl font-bold text-white">
<TextTyper
text="Transaction Volume"
typingSpeed={30}
delay={1500}
/>
</h3>
<p className="text-center text-zinc-400">
<TextTyper
text="Total value of successful transactions processed"
typingSpeed={10}
delay={2500}
/>
</p>
</div>
</div>
);
}

87
components/text-typer.tsx Normal file
View File

@@ -0,0 +1,87 @@
"use client";
import { useState, useEffect, useRef } from "react";
interface TextTyperProps {
text: string;
typingSpeed?: number;
delay?: number;
className?: string;
cursor?: boolean;
onComplete?: () => void;
}
export function TextTyper({
text,
typingSpeed = 50,
delay = 0,
className = "",
cursor = true,
onComplete
}: TextTyperProps) {
// Hide component if text is empty
if (!text) return null;
const [displayedText, setDisplayedText] = useState("");
const [cursorVisible, setCursorVisible] = useState(true);
const [isComplete, setIsComplete] = useState(false);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const cursorIntervalRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
// Clear any previous text
setDisplayedText("");
setIsComplete(false);
// Reset typing when text changes
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
// Start typing after delay
const startTypingTimeout = setTimeout(() => {
let currentIndex = 0;
const typeNextChar = () => {
if (currentIndex < text.length) {
setDisplayedText(text.substring(0, currentIndex + 1));
currentIndex++;
timeoutRef.current = setTimeout(typeNextChar, typingSpeed);
} else {
setIsComplete(true);
if (onComplete) {
onComplete();
}
}
};
typeNextChar();
}, delay);
// Store the start timeout
timeoutRef.current = startTypingTimeout;
// Cursor blinking effect
if (cursor) {
cursorIntervalRef.current = setInterval(() => {
setCursorVisible(prev => !prev);
}, 500);
}
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
if (cursorIntervalRef.current) {
clearInterval(cursorIntervalRef.current);
}
};
}, [text, typingSpeed, delay, cursor, onComplete]);
return (
<span className={className}>
{displayedText}
{cursor && !isComplete && cursorVisible && <span className="text-[#D53F8C]">|</span>}
</span>
);
}