Compare commits

...

2 commits

Author SHA1 Message Date
rocord01 89ec46f827 statistics wowowoowowowow
All checks were successful
Deploy to Server / deploy (push) Successful in 2m19s
2025-07-26 23:05:09 -04:00
rocord01 4333d2cd11 auto answer 2025-07-26 22:38:32 -04:00
3 changed files with 235 additions and 0 deletions

View file

@ -5,6 +5,7 @@ import Features from '@/components/features'
import Team from '@/components/team' import Team from '@/components/team'
import Footer from '@/components/footer' import Footer from '@/components/footer'
import Updates from '@/components/updates' // Import Blog component import Updates from '@/components/updates' // Import Blog component
import HomeTickers from '@/components/HomeTickers'
export default function Home() { export default function Home() {
return ( return (
@ -13,6 +14,7 @@ export default function Home() {
<main> <main>
<Hero /> <Hero />
<Features /> <Features />
<HomeTickers />
<Updates /> <Updates />
<Team /> <Team />
</main> </main>

View 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>
);
}

View file

@ -17,6 +17,7 @@ export function QuickActionsCard({ details, loading }) {
const [isCalling, setIsCalling] = useState(false); const [isCalling, setIsCalling] = useState(false);
const [callMode, setCallMode] = useState('hold'); const [callMode, setCallMode] = useState('hold');
const [callerId, setCallerId] = useState(''); const [callerId, setCallerId] = useState('');
const [autoAnswerMode, setAutoAnswerMode] = useState('none');
const [isCallMeDialogOpen, setIsCallMeDialogOpen] = useState(false); const [isCallMeDialogOpen, setIsCallMeDialogOpen] = useState(false);
const handleCallMe = async () => { const handleCallMe = async () => {
@ -27,6 +28,9 @@ export function QuickActionsCard({ details, loading }) {
if (callerId) { if (callerId) {
url += `&callerId=${encodeURIComponent(callerId)}`; url += `&callerId=${encodeURIComponent(callerId)}`;
} }
if (autoAnswerMode !== 'none') {
url += `&autoAnswerMode=${autoAnswerMode}`;
}
await fetch(url, { await fetch(url, {
method: 'POST', method: 'POST',
headers: { 'Authorization': `Bearer ${token}` } headers: { 'Authorization': `Bearer ${token}` }
@ -89,6 +93,28 @@ export function QuickActionsCard({ details, loading }) {
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<div className="grid grid-cols-1 gap-2 sm:grid-cols-4 sm:items-center sm:gap-4">
<Label htmlFor="autoAnswerMode" className="text-sm font-medium sm:text-right">
Auto Answer
</Label>
<Select value={autoAnswerMode} onValueChange={setAutoAnswerMode}>
<SelectTrigger className="sm:col-span-3">
<SelectValue placeholder="Select an auto-answer mode" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">None</SelectItem>
<SelectItem value="default">
Default <span className="text-gray-400 ml-2">(Yealink, Grandstream)</span>
</SelectItem>
<SelectItem value="intercom">
Intercom <span className="text-gray-400 ml-2">(Polycom, Snom)</span>
</SelectItem>
<SelectItem value="ring-answer">
Ring Answer <span className="text-gray-400 ml-2">(Other devices)</span>
</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-1 gap-2 sm:grid-cols-4 sm:items-center sm:gap-4"> <div className="grid grid-cols-1 gap-2 sm:grid-cols-4 sm:items-center sm:gap-4">
<Label htmlFor="callerId" className="text-sm font-medium sm:text-right"> <Label htmlFor="callerId" className="text-sm font-medium sm:text-right">
Caller ID Caller ID