statistics wowowoowowowow
All checks were successful
Deploy to Server / deploy (push) Successful in 2m19s
All checks were successful
Deploy to Server / deploy (push) Successful in 2m19s
This commit is contained in:
parent
4333d2cd11
commit
89ec46f827
|
@ -5,6 +5,7 @@ import Features from '@/components/features'
|
|||
import Team from '@/components/team'
|
||||
import Footer from '@/components/footer'
|
||||
import Updates from '@/components/updates' // Import Blog component
|
||||
import HomeTickers from '@/components/HomeTickers'
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
|
@ -13,6 +14,7 @@ export default function Home() {
|
|||
<main>
|
||||
<Hero />
|
||||
<Features />
|
||||
<HomeTickers />
|
||||
<Updates />
|
||||
<Team />
|
||||
</main>
|
||||
|
|
207
src/components/HomeTickers.jsx
Normal file
207
src/components/HomeTickers.jsx
Normal file
|
@ -0,0 +1,207 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { TrendingUp, PhoneCall, CalendarDays } from "lucide-react";
|
||||
import { format } from "date-fns";
|
||||
|
||||
function formatNumber(num) {
|
||||
return num?.toLocaleString() ?? '0';
|
||||
}
|
||||
|
||||
const tickerStyles = {
|
||||
blue: 'bg-blue-600/20 border-blue-600/30 text-blue-400 group-hover:bg-blue-600/30',
|
||||
green: 'bg-green-600/20 border-green-600/30 text-green-400 group-hover:bg-green-600/30',
|
||||
purple: 'bg-purple-600/20 border-purple-600/30 text-purple-400 group-hover:bg-purple-600/30',
|
||||
};
|
||||
|
||||
function useAnimatedNumber(target, duration = 1200, shouldAnimate = true) {
|
||||
const [display, setDisplay] = useState(0);
|
||||
const rafRef = useRef();
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldAnimate || typeof target !== "number") {
|
||||
setDisplay(target || 0);
|
||||
return;
|
||||
}
|
||||
let start = null;
|
||||
let from = 0;
|
||||
let to = target;
|
||||
if (from === to) {
|
||||
setDisplay(to);
|
||||
return;
|
||||
}
|
||||
|
||||
function tick(ts) {
|
||||
if (!start) start = ts;
|
||||
const progress = Math.min((ts - start) / duration, 1);
|
||||
const value = Math.round(from + (to - from) * progress);
|
||||
setDisplay(value);
|
||||
if (progress < 1) {
|
||||
rafRef.current = requestAnimationFrame(tick);
|
||||
}
|
||||
}
|
||||
rafRef.current = requestAnimationFrame(tick);
|
||||
return () => rafRef.current && cancelAnimationFrame(rafRef.current);
|
||||
// eslint-disable-next-line
|
||||
}, [target, shouldAnimate]);
|
||||
return display;
|
||||
}
|
||||
|
||||
function AnimatedNumber({ value, animate, ...props }) {
|
||||
const animated = useAnimatedNumber(typeof value === "number" ? value : 0, 1200, animate);
|
||||
return <span {...props}>{animated.toLocaleString()}</span>;
|
||||
}
|
||||
|
||||
function formatMonthLabel(monthStr) {
|
||||
// monthStr is like "2025-07"
|
||||
const [year, month] = monthStr.split("-");
|
||||
if (!year || !month) return monthStr;
|
||||
const date = new Date(Number(year), Number(month) - 1, 1);
|
||||
return format(date, "MMMM yyyy");
|
||||
}
|
||||
|
||||
function formatRecordDate(dateStr) {
|
||||
// dateStr is like "2024-10-28"
|
||||
if (!dateStr) return "";
|
||||
const [year, month, day] = dateStr.split("-");
|
||||
if (!year || !month || !day) return dateStr;
|
||||
const date = new Date(Number(year), Number(month) - 1, Number(day));
|
||||
return format(date, "MMMM do, yyyy");
|
||||
}
|
||||
|
||||
function TickerCard({ color, icon, title, value, subtitle, loading, animate, children }) {
|
||||
return (
|
||||
<div className="group relative bg-gray-950/50 border border-gray-800 hover:border-blue-500/50 transition-colors duration-500 ease-out hover:shadow-2xl hover:shadow-blue-500/10 overflow-hidden rounded-lg">
|
||||
{/* Animated background gradient */}
|
||||
<div className={`absolute inset-0 bg-gradient-to-br from-transparent via-blue-500/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500 rounded-lg`} />
|
||||
<div className="flex flex-col items-center justify-center px-6 py-8 min-h-[170px]">
|
||||
<div className={`rounded-xl p-3 border transition-all duration-300 mb-3 ${tickerStyles[color]}`}>
|
||||
{icon}
|
||||
</div>
|
||||
<div className="text-xl font-bold text-white mb-2">{title}</div>
|
||||
{loading ? (
|
||||
<Skeleton className="h-12 w-32 mb-2" />
|
||||
) : (
|
||||
value !== undefined && value !== "" && value !== null ? (
|
||||
<AnimatedNumber className={`text-3xl font-extrabold ${tickerStyles[color].split(' ')[2]} tracking-tight mb-2`} value={Number(value)} animate={animate} />
|
||||
) : null
|
||||
)}
|
||||
{subtitle && <span className="text-xs text-gray-400 mt-1">{subtitle}</span>}
|
||||
{children}
|
||||
</div>
|
||||
{/* Hover glow effect */}
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-blue-500/0 via-blue-500/0 to-blue-500/0 group-hover:from-blue-500/5 group-hover:via-transparent group-hover:to-blue-500/5 transition-all duration-500 pointer-events-none rounded-lg" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function HomeTickers() {
|
||||
const [stats, setStats] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [animateNumbers, setAnimateNumbers] = useState(false);
|
||||
const sectionRef = useRef();
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchStats() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/system/records`);
|
||||
const data = await res.json();
|
||||
setStats(data.records);
|
||||
} catch (err) {
|
||||
setStats(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
fetchStats();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const ref = sectionRef.current;
|
||||
if (!ref) return;
|
||||
let observer;
|
||||
if (typeof window !== "undefined" && "IntersectionObserver" in window) {
|
||||
observer = new window.IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setAnimateNumbers(true);
|
||||
}
|
||||
},
|
||||
{ threshold: 0.4 }
|
||||
);
|
||||
observer.observe(ref);
|
||||
}
|
||||
return () => observer && observer.disconnect();
|
||||
}, []);
|
||||
|
||||
const monthlyStats = stats
|
||||
? Object.entries(stats)
|
||||
.filter(([k, v]) => k.startsWith("monthly_total_") && v > 0)
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
: [];
|
||||
|
||||
// Get current month in YYYY-MM format
|
||||
const now = new Date();
|
||||
const currentMonthKey = `monthly_total_${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
|
||||
const currentMonthCount = stats?.[currentMonthKey];
|
||||
|
||||
return (
|
||||
<section ref={sectionRef} className="container py-12 px-2 sm:px-6">
|
||||
<div className="text-center mb-10">
|
||||
<h2 className="text-3xl sm:text-4xl md:text-5xl font-bold text-white mb-4 bg-gradient-to-r from-white via-blue-100 to-white bg-clip-text text-transparent pb-2">
|
||||
Network Activity
|
||||
</h2>
|
||||
<p className="text-lg sm:text-xl text-gray-300 leading-relaxed">
|
||||
Real-time call statistics from the LiteNet PBX.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-6 sm:gap-8 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<TickerCard
|
||||
color="blue"
|
||||
icon={<CalendarDays className="h-7 w-7 text-blue-400" />}
|
||||
title="This Month's Calls"
|
||||
value={currentMonthCount}
|
||||
subtitle={format(now, "MMMM yyyy")}
|
||||
loading={loading}
|
||||
animate={animateNumbers}
|
||||
/>
|
||||
<TickerCard
|
||||
color="green"
|
||||
icon={<TrendingUp className="h-7 w-7 text-green-400" />}
|
||||
title="Total Calls"
|
||||
value={stats?.total_calls_ever_placed}
|
||||
subtitle="Since launch"
|
||||
loading={loading}
|
||||
animate={animateNumbers}
|
||||
/>
|
||||
<TickerCard
|
||||
color="purple"
|
||||
icon={<PhoneCall className="h-7 w-7 text-purple-400" />}
|
||||
title="Single-Day Call Record"
|
||||
value={stats?.record_calls?.count}
|
||||
subtitle={
|
||||
stats?.record_calls?.date
|
||||
? `Record set on ${formatRecordDate(stats.record_calls.date)}`
|
||||
: ""
|
||||
}
|
||||
loading={loading}
|
||||
animate={animateNumbers}
|
||||
/>
|
||||
</div>
|
||||
<style jsx>{`
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</section>
|
||||
);
|
||||
}
|
Loading…
Reference in a new issue