Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/admin/components/statsCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export function StatsCards({ data }: StatsCardsProps) {
},
{
title: 'Total Confirmed Amount',
value: calculatedUsdtBal(),
value: data.totalConfirmedAmount,
description: 'Across all transactions',
},
{
Expand Down
237 changes: 231 additions & 6 deletions app/stats/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,234 @@
import React from 'react'
import DashboardPage from '../admin/dashboard/page'
'use client';

import { useEffect, useState } from 'react';
import { StatsCards } from '../admin/components/statsCard';
import { ChainStats } from '../admin/components/chain-stats';
import { UsageStats } from '@/app/admin/components/usage-stats';
import { StatsData } from '@/types/admin';
import { getStats } from '@/lib/api';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/cards';
import { Button } from '@/components/ui/buttons';
import { RefreshCw } from 'lucide-react';

// Cache key for localStorage
const STATS_CACHE_KEY = 'admin_dashboard_stats';
const CACHE_DURATION = 5 * 60 * 1000;

interface CachedStats {
data: StatsData;
timestamp: number;
}

export default function DashboardPage() {
const [stats, setStats] = useState<StatsData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [isRefreshing, setIsRefreshing] = useState(false);

// Get cached data
const getCachedStats = (): CachedStats | null => {
if (typeof window === 'undefined') return null;

try {
const cached = localStorage.getItem(STATS_CACHE_KEY);
if (!cached) return null;

const parsed: CachedStats = JSON.parse(cached);
const isExpired = Date.now() - parsed.timestamp > CACHE_DURATION;

return isExpired ? null : parsed;
} catch {
return null;
}
};

// Save data to cache
const setCachedStats = (data: StatsData) => {
if (typeof window === 'undefined') return;

const cachedData: CachedStats = {
data,
timestamp: Date.now()
};

localStorage.setItem(STATS_CACHE_KEY, JSON.stringify(cachedData));
};

// Clear cache
const clearCache = () => {
if (typeof window === 'undefined') return;
localStorage.removeItem(STATS_CACHE_KEY);
};

const fetchStats = async (backgroundRefresh = false) => {
if (!backgroundRefresh) {
setLoading(true);
} else {
setIsRefreshing(true);
}

setError(null);

try {
// Try to use cached data first (only for initial load)
if (!backgroundRefresh) {
const cached = getCachedStats();
if (cached) {
setStats(cached.data);
setLoading(false);

// Background refresh if cache is older than 2 minutes
const cacheAge = Date.now() - cached.timestamp;
if (cacheAge > 2 * 60 * 1000) {
fetchStats(true);
}
return;
}
}

// Fetch fresh data
const data = await getStats();
setStats(data);
setCachedStats(data);

} catch (err) {
// Only show error if it's not a background refresh
if (!backgroundRefresh) {
setError(err instanceof Error ? err.message : 'Failed to fetch stats');
} else {
console.error('Background refresh failed:', err);
}
} finally {
if (!backgroundRefresh) {
setLoading(false);
} else {
setIsRefreshing(false);
}
}
};

// Manual refresh handler
const handleManualRefresh = async () => {
clearCache();
await fetchStats(false);
};

useEffect(() => {
fetchStats(false);

// Set up periodic background refresh every 5 minutes
const interval = setInterval(() => {
fetchStats(true);
}, 5 * 60 * 1000);

return () => clearInterval(interval);
}, []);

if (loading) {
return (
<div className="flex min-h-screen items-center justify-center">
<div className="text-center">
<RefreshCw className="mx-auto h-8 w-8 animate-spin" />
<p className="mt-2">Loading dashboard...</p>
</div>
</div>
);
}

if (error) {
return (
<div className="flex min-h-screen items-center justify-center">
<Card>
<CardContent className="pt-6">
<div className="text-center">
<p className="text-red-500">Error: {error}</p>
<Button onClick={handleManualRefresh} className="mt-4">
Try Again
</Button>
</div>
</CardContent>
</Card>
</div>
);
}

if (!stats) {
return (
<div className="flex min-h-screen items-center justify-center">
<p>No data available</p>
</div>
);
}

export default function Stats() {
return (
<div>
<div className="container mx-auto py-6">
<div className="mb-6 flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight">Admin Dashboard</h1>
<p className="text-muted-foreground">
Overview of platform statistics and usage
{isRefreshing && (
<span className="ml-2 text-sm text-blue-500">
<RefreshCw className="inline h-3 w-3 animate-spin mr-1" />
Updating...
</span>
)}
</p>
</div>
<Button
onClick={handleManualRefresh}
variant="outline"
size="sm"
disabled={isRefreshing}
>
<RefreshCw className={`mr-2 h-4 w-4 ${isRefreshing ? 'animate-spin' : ''}`} />
{isRefreshing ? 'Refreshing...' : 'Refresh'}
</Button>
</div>

<div className="space-y-6">
<StatsCards data={stats} />

<div className="grid gap-6 md:grid-cols-2">
<ChainStats data={stats} />
<UsageStats data={stats} />
</div>

{/* Summary Card */}
<Card>
<CardHeader>
<CardTitle>Platform Summary</CardTitle>
<CardDescription>
Current overview of platform performance and user activity
{getCachedStats() && (
<span className="text-xs text-green-600 ml-2">
• Cached data (updates every 5 min)
</span>
)}
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid gap-4 text-sm md:grid-cols-2">
<div>
<h4 className="font-semibold">Transaction Summary</h4>
<ul className="mt-2 space-y-1">
<li>Total transactions across all chains: {stats.perChain.reduce((sum, chain) => sum + chain.count, 0)}</li>
<li>Most active chain: {stats.mostUsedChain}</li>
<li>Total confirmed amount: {stats.totalConfirmedAmount}</li>
</ul>
</div>
<div>
<h4 className="font-semibold">Feature Usage</h4>
<ul className="mt-2 space-y-1">
<li>Most used feature: Splitting ({stats.usage.splitting} times)</li>
<li>Send operations: {stats.usage.send}</li>
<li>QR Payments: {stats.usage.qrPayment}</li>
</ul>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
)
}
);
}
2 changes: 1 addition & 1 deletion components/dashboard/tabs/qr-payment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ export default function QrPayment() {
const requestBody: any = {
amount: parseFloat(tokenAmount),
chain: chain,
network: "testnet",
network: "mainnet",
description: description || "QR Payment request",
};

Expand Down
4 changes: 2 additions & 2 deletions components/dashboard/tabs/send-funds.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,9 @@ export default function SendFunds() {
}, [addresses, selectedToken]);

const currentNetwork = useMemo(() => {
if (!addresses) return "testnet";
if (!addresses) return "mainnet";
const addressInfo = addresses.find((addr) => addr.chain === selectedToken);
return addressInfo?.network || "testnet";
return addressInfo?.network || "mainnet";
}, [addresses, selectedToken]);

// Check if selected token has a wallet
Expand Down
2 changes: 2 additions & 0 deletions components/hooks/useNotifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ export const useNotifications = () => {
const [notifications, setNotifications] = useState<FrontendNotification[]>([]);
const [unreadCount, setUnreadCount] = useState<number>(0);

// console.log("XXXXXXXXXXXXXXXXXXXXXXXXXX",notifications)

// Use useRef to track shown notifications - persists across renders without causing re-renders
const shownNotificationIds = useRef<Set<string>>(new Set());
const isInitialMount = useRef(true);
Expand Down
4 changes: 2 additions & 2 deletions components/hooks/useTokenBalance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ export function useTokenBalance() {

const getWalletNetwork = useCallback(
(chain: string): string => {
if (!addresses || !Array.isArray(addresses)) return "testnet";
if (!addresses || !Array.isArray(addresses)) return "mainnet";
const addressInfo = addresses.find((addr) => addr.chain === chain);
return addressInfo?.network || "testnet";
return addressInfo?.network || "mainnet";
},
[addresses]
);
Expand Down
11 changes: 11 additions & 0 deletions components/landingpage/landing/navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,17 @@ export function Navigation() {
blog
</Link>
</NavigationMenuItem>
<NavigationMenuItem>
<Link
href="/stats"
className={cn(
"inline-flex h-9 w-max items-center justify-center rounded-md px-4 py-2 text-sm font-medium transition-colors hover:bg-white/10",
scrolled ? "text-foreground hover:text-foreground" : "text-white hover:text-white",
)}
>
Stats
</Link>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
</div>
Expand Down
2 changes: 1 addition & 1 deletion components/modals/add-split.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export default function AddSplit({ close, onSuccess }: AddSplitProps) {
});

const [errors, setErrors] = useState<Record<string, string>>({});
const network = "testnet";
const network = "mainnet";

// CHANGED: Use addresses from useWalletData
const availableChains = [...new Set(addresses.map(a => a.chain))];
Expand Down
8 changes: 4 additions & 4 deletions lib/api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ class ApiClient {
// Wallet methods
async getWalletAddresses(): Promise<WalletAddress[]> {
return this.request<{ addresses: WalletAddress[] }>(
"/wallet/addresses/testnet",
"/wallet/addresses/mainnet",
{ method: "GET" },
{
ttl: 10 * 60 * 1000, // 10 minutes - addresses don't change often
Expand All @@ -266,7 +266,7 @@ class ApiClient {

async getWalletBalances(): Promise<WalletBalance[]> {
return this.request<{ balances: WalletBalance[] }>(
"/wallet/balances/testnet",
"/wallet/balances/mainnet",
{ method: "GET" },
{
ttl: 2 * 60 * 1000, // 2 minutes - balances change more frequently
Expand All @@ -282,7 +282,7 @@ class ApiClient {
});

// Invalidate balance cache after sending transaction
this.cache.invalidateCache(["/wallet/balances/testnet"]);
this.cache.invalidateCache(["/wallet/balances/mainnet"]);
return result;
}

Expand Down Expand Up @@ -539,7 +539,7 @@ async executeSplitPayment(id: string, pin: string): Promise<ExecuteSplitPaymentR

async checkDeploy(): Promise<DepositCheckResponse> {
return this.request<DepositCheckResponse>(
"/checkdeploy/balances/testnet/deploy",
"/checkdeploy/balances/mainnet/deploy",
{
method: "GET",
},
Expand Down