ugh
This commit is contained in:
29
app/page.tsx
29
app/page.tsx
@@ -1,10 +1,12 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ArrowRight, Shield, LineChart, Zap, Package, Users, CreditCard } from "lucide-react";
|
import { ArrowRight, Shield, LineChart, Zap } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { fetchPlatformStats } from "@/lib/stats-service";
|
import { fetchPlatformStats } from "@/lib/stats-service";
|
||||||
import { HomeNavbar } from "@/components/home-navbar";
|
import { HomeNavbar } from "@/components/home-navbar";
|
||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
|
import { AnimatedStatsSection } from "@/components/animated-stats-section";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { TextTyper } from "@/components/text-typer";
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
const PY_20 = 20;
|
const PY_20 = 20;
|
||||||
@@ -64,26 +66,7 @@ export default async function Home() {
|
|||||||
<section className="py-16 px-6 md:px-10 bg-black">
|
<section className="py-16 px-6 md:px-10 bg-black">
|
||||||
<div className="max-w-6xl mx-auto">
|
<div className="max-w-6xl mx-auto">
|
||||||
<Suspense fallback={<div className="text-center">Loading statistics...</div>}>
|
<Suspense fallback={<div className="text-center">Loading statistics...</div>}>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
<AnimatedStatsSection stats={stats} />
|
||||||
<div className="flex flex-col items-center text-center p-6 rounded-md bg-[#1C1C1C] border-0">
|
|
||||||
<Package className="h-12 w-12 text-[#D53F8C] mb-4" />
|
|
||||||
<div className="text-4xl font-bold text-[#D53F8C] mb-2">{formatNumberValue(stats.orders.completed)}+</div>
|
|
||||||
<div className="text-xl font-semibold text-white mb-1">Orders Processed</div>
|
|
||||||
<p className="text-sm text-gray-400">Successfully delivered to customers around the world</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col items-center text-center p-6 rounded-md bg-[#1C1C1C] border-0">
|
|
||||||
<Users className="h-12 w-12 text-[#D53F8C] mb-4" />
|
|
||||||
<div className="text-4xl font-bold text-[#D53F8C] mb-2">{formatNumberValue(stats.vendors.total)}+</div>
|
|
||||||
<div className="text-xl font-semibold text-white mb-1">Registered Vendors</div>
|
|
||||||
<p className="text-sm text-gray-400">Trusted merchants selling on our platform</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col items-center text-center p-6 rounded-md bg-[#1C1C1C] border-0">
|
|
||||||
<CreditCard className="h-12 w-12 text-[#D53F8C] mb-4" />
|
|
||||||
<div className="text-4xl font-bold text-[#D53F8C] mb-2">{formatCurrencyValue(stats.transactions.volume)}</div>
|
|
||||||
<div className="text-xl font-semibold text-white mb-1">Transaction Volume</div>
|
|
||||||
<p className="text-sm text-gray-400">Securely processed by our platform</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -187,7 +170,7 @@ export default async function Home() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-10 text-center text-sm text-gray-500">
|
<div className="mt-10 text-center text-sm text-gray-500">
|
||||||
<p> {new Date().getFullYear()} Ember. All rights reserved.</p>
|
<p>© {new Date().getFullYear()} Ember. All rights reserved.</p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
71
components/animated-counter.tsx
Normal file
71
components/animated-counter.tsx
Normal 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>;
|
||||||
|
}
|
||||||
105
components/animated-stats-section.tsx
Normal file
105
components/animated-stats-section.tsx
Normal 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
87
components/text-typer.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -14,6 +14,8 @@ export interface PlatformStats {
|
|||||||
export async function fetchPlatformStats(): Promise<PlatformStats> {
|
export async function fetchPlatformStats(): Promise<PlatformStats> {
|
||||||
const BASE_API_URL = process.env.SERVER_API_URL || 'http://localhost:3001/api';
|
const BASE_API_URL = process.env.SERVER_API_URL || 'http://localhost:3001/api';
|
||||||
|
|
||||||
|
console.log('Fetching platform stats from:', BASE_API_URL);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${BASE_API_URL}/stats/platform`, {
|
const response = await fetch(`${BASE_API_URL}/stats/platform`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@@ -27,7 +29,9 @@ export async function fetchPlatformStats(): Promise<PlatformStats> {
|
|||||||
throw new Error(`API error: ${response.status}`);
|
throw new Error(`API error: ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await response.json();
|
const data = await response.json();
|
||||||
|
console.log('Fetched stats:', data);
|
||||||
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching platform stats:', error);
|
console.error('Error fetching platform stats:', error);
|
||||||
// Return fallback data if API fails
|
// Return fallback data if API fails
|
||||||
|
|||||||
Reference in New Issue
Block a user