Skip to content

Commit 1913233

Browse files
committed
feat(frontend): add skeleton loading states to all data-heavy pages
- Create TransactionTableSkeleton for transactions page - Create NotificationListSkeleton for notifications page - Create AdminOverviewSkeleton for admin overview page - Create AdminTableSkeleton for admin escrows page - Create AdminDisputesSkeleton for admin disputes page - Replace Suspense fallback on dashboard with EscrowCardSkeleton - Replace all Loader2 spinners with matching skeleton components - Skeletons match layout shape of actual content Closes #478
1 parent fbf996b commit 1913233

12 files changed

Lines changed: 893 additions & 686 deletions

File tree

apps/frontend/app/admin/disputes/page.tsx

Lines changed: 88 additions & 148 deletions
Large diffs are not rendered by default.

apps/frontend/app/admin/escrows/page.tsx

Lines changed: 162 additions & 115 deletions
Large diffs are not rendered by default.

apps/frontend/app/admin/page.tsx

Lines changed: 31 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,10 @@
1-
'use client';
1+
"use client";
22

3-
import React, { useState, useEffect } from 'react';
4-
import {
5-
Users,
6-
Shield,
7-
TrendingUp,
8-
Activity,
9-
ArrowUpRight,
10-
ArrowDownRight,
11-
BarChart3,
12-
Wallet,
13-
UserPlus,
14-
CheckCircle2,
15-
Loader2,
16-
} from 'lucide-react';
17-
import { AdminService } from '@/services/admin';
18-
import { IPlatformStats } from '@/types/admin';
3+
import React, { useState, useEffect } from "react";
4+
import { Users, Shield, TrendingUp, Activity, ArrowUpRight, ArrowDownRight, BarChart3, Wallet, UserPlus, CheckCircle2 } from "lucide-react";
5+
import { AdminService } from "@/services/admin";
6+
import { IPlatformStats } from "@/types/admin";
7+
import { AdminOverviewSkeleton } from "@/components/ui/AdminOverviewSkeleton";
198

209
function StatCard({
2110
title,
@@ -35,41 +24,29 @@ function StatCard({
3524
const isPositive = change && change > 0;
3625
return (
3726
<div className="relative group">
38-
<div className="absolute -inset-[1px] bg-gradient-to-r opacity-0 group-hover:opacity-100 rounded-xl transition-opacity duration-500 blur-sm"
27+
<div
28+
className="absolute -inset-[1px] bg-gradient-to-r opacity-0 group-hover:opacity-100 rounded-xl transition-opacity duration-500 blur-sm"
3929
style={{ backgroundImage: gradient }}
4030
/>
4131
<div className="relative bg-[#12121a] border border-white/5 rounded-xl p-5 hover:border-white/10 transition-all duration-300">
4232
<div className="flex items-start justify-between mb-4">
43-
<div
44-
className="w-10 h-10 rounded-lg flex items-center justify-center"
45-
style={{ background: gradient }}
46-
>
33+
<div className="w-10 h-10 rounded-lg flex items-center justify-center" style={{ background: gradient }}>
4734
<Icon className="w-5 h-5 text-white" />
4835
</div>
4936
{change !== undefined && (
5037
<div
5138
className={`flex items-center gap-1 text-xs font-medium px-2 py-1 rounded-full ${
52-
isPositive
53-
? 'bg-emerald-500/10 text-emerald-400'
54-
: 'bg-red-500/10 text-red-400'
39+
isPositive ? "bg-emerald-500/10 text-emerald-400" : "bg-red-500/10 text-red-400"
5540
}`}
5641
>
57-
{isPositive ? (
58-
<ArrowUpRight className="w-3 h-3" />
59-
) : (
60-
<ArrowDownRight className="w-3 h-3" />
61-
)}
42+
{isPositive ? <ArrowUpRight className="w-3 h-3" /> : <ArrowDownRight className="w-3 h-3" />}
6243
{Math.abs(change)}%
6344
</div>
6445
)}
6546
</div>
66-
<p className="text-2xl font-bold text-white mb-1">
67-
{typeof value === 'number' ? value.toLocaleString() : value}
68-
</p>
47+
<p className="text-2xl font-bold text-white mb-1">{typeof value === "number" ? value.toLocaleString() : value}</p>
6948
<p className="text-xs text-gray-500">{title}</p>
70-
{changeLabel && (
71-
<p className="text-[10px] text-gray-600 mt-1">{changeLabel}</p>
72-
)}
49+
{changeLabel && <p className="text-[10px] text-gray-600 mt-1">{changeLabel}</p>}
7350
</div>
7451
</div>
7552
);
@@ -78,9 +55,9 @@ function StatCard({
7855
function RoleDistribution({ roles }: { roles: Record<string, number> }) {
7956
const total = Object.values(roles).reduce((a, b) => a + b, 0);
8057
const colors: Record<string, string> = {
81-
USER: '#8b5cf6',
82-
ADMIN: '#3b82f6',
83-
SUPER_ADMIN: '#06b6d4',
58+
USER: "#8b5cf6",
59+
ADMIN: "#3b82f6",
60+
SUPER_ADMIN: "#06b6d4",
8461
};
8562

8663
return (
@@ -95,9 +72,7 @@ function RoleDistribution({ roles }: { roles: Record<string, number> }) {
9572
return (
9673
<div key={role}>
9774
<div className="flex items-center justify-between mb-1.5">
98-
<span className="text-xs text-gray-400 capitalize">
99-
{role.replace('_', ' ').toLowerCase()}
100-
</span>
75+
<span className="text-xs text-gray-400 capitalize">{role.replace("_", " ").toLowerCase()}</span>
10176
<span className="text-xs text-gray-300 font-medium">
10277
{count.toLocaleString()} ({pct.toFixed(1)}%)
10378
</span>
@@ -107,7 +82,7 @@ function RoleDistribution({ roles }: { roles: Record<string, number> }) {
10782
className="h-full rounded-full transition-all duration-1000 ease-out"
10883
style={{
10984
width: `${pct}%`,
110-
backgroundColor: colors[role] || '#8b5cf6',
85+
backgroundColor: colors[role] || "#8b5cf6",
11186
}}
11287
/>
11388
</div>
@@ -121,7 +96,7 @@ function RoleDistribution({ roles }: { roles: Record<string, number> }) {
12196

12297
function EscrowVolumeChart() {
12398
// Simulated chart data
124-
const months = ['Oct', 'Nov', 'Dec', 'Jan', 'Feb', 'Mar'];
99+
const months = ["Oct", "Nov", "Dec", "Jan", "Feb", "Mar"];
125100
const values = [320, 480, 560, 720, 610, 850];
126101
const max = Math.max(...values);
127102

@@ -137,9 +112,7 @@ function EscrowVolumeChart() {
137112
const height = (values[i] / max) * 100;
138113
return (
139114
<div key={month} className="flex-1 flex flex-col items-center gap-2">
140-
<span className="text-[10px] text-gray-500 font-medium">
141-
{values[i]}
142-
</span>
115+
<span className="text-[10px] text-gray-500 font-medium">{values[i]}</span>
143116
<div className="w-full relative rounded-t-md overflow-hidden" style={{ height: `${height}%` }}>
144117
<div
145118
className="absolute inset-0 rounded-t-md transition-all duration-700 ease-out"
@@ -159,11 +132,11 @@ function EscrowVolumeChart() {
159132

160133
function RecentActivity() {
161134
const activities = [
162-
{ type: 'escrow_created', desc: 'New escrow created: "Website Dev"', time: '5m ago', icon: Shield },
163-
{ type: 'user_joined', desc: 'New user registered: GAX3...', time: '12m ago', icon: UserPlus },
164-
{ type: 'escrow_completed', desc: 'Escrow completed: "API Integration"', time: '28m ago', icon: CheckCircle2 },
165-
{ type: 'user_joined', desc: 'New user registered: GBK2...', time: '1h ago', icon: UserPlus },
166-
{ type: 'escrow_created', desc: 'New escrow created: "DeFi Audit"', time: '2h ago', icon: Shield },
135+
{ type: "escrow_created", desc: 'New escrow created: "Website Dev"', time: "5m ago", icon: Shield },
136+
{ type: "user_joined", desc: "New user registered: GAX3...", time: "12m ago", icon: UserPlus },
137+
{ type: "escrow_completed", desc: 'Escrow completed: "API Integration"', time: "28m ago", icon: CheckCircle2 },
138+
{ type: "user_joined", desc: "New user registered: GBK2...", time: "1h ago", icon: UserPlus },
139+
{ type: "escrow_created", desc: 'New escrow created: "DeFi Audit"', time: "2h ago", icon: Shield },
167140
];
168141

169142
return (
@@ -203,14 +176,7 @@ export default function AdminOverviewPage() {
203176
}, []);
204177

205178
if (loading) {
206-
return (
207-
<div className="flex items-center justify-center min-h-[60vh]">
208-
<div className="flex flex-col items-center gap-3">
209-
<Loader2 className="w-8 h-8 text-purple-500 animate-spin" />
210-
<p className="text-sm text-gray-500">Loading dashboard...</p>
211-
</div>
212-
</div>
213-
);
179+
return <AdminOverviewSkeleton />;
214180
}
215181

216182
if (!stats) return null;
@@ -220,9 +186,7 @@ export default function AdminOverviewPage() {
220186
{/* Page header */}
221187
<div>
222188
<h1 className="text-2xl font-bold text-white">Platform Overview</h1>
223-
<p className="text-sm text-gray-500 mt-1">
224-
Real-time platform statistics and monitoring
225-
</p>
189+
<p className="text-sm text-gray-500 mt-1">Real-time platform statistics and monitoring</p>
226190
</div>
227191

228192
{/* Stats grid */}
@@ -278,34 +242,19 @@ export default function AdminOverviewPage() {
278242
</h3>
279243
<div className="grid grid-cols-2 gap-4">
280244
<div className="bg-white/[0.02] rounded-lg p-4">
281-
<p className="text-2xl font-bold text-white">
282-
{stats.users.active.toLocaleString()}
283-
</p>
245+
<p className="text-2xl font-bold text-white">{stats.users.active.toLocaleString()}</p>
284246
<p className="text-xs text-gray-500 mt-1">Active Users</p>
285247
</div>
286248
<div className="bg-white/[0.02] rounded-lg p-4">
287-
<p className="text-2xl font-bold text-white">
288-
{stats.escrows.total.toLocaleString()}
289-
</p>
249+
<p className="text-2xl font-bold text-white">{stats.escrows.total.toLocaleString()}</p>
290250
<p className="text-xs text-gray-500 mt-1">Total Escrows</p>
291251
</div>
292252
<div className="bg-white/[0.02] rounded-lg p-4">
293-
<p className="text-2xl font-bold text-white">
294-
{(
295-
(stats.escrows.completed / Math.max(stats.escrows.total, 1)) *
296-
100
297-
).toFixed(1)}
298-
%
299-
</p>
253+
<p className="text-2xl font-bold text-white">{((stats.escrows.completed / Math.max(stats.escrows.total, 1)) * 100).toFixed(1)}%</p>
300254
<p className="text-xs text-gray-500 mt-1">Completion Rate</p>
301255
</div>
302256
<div className="bg-white/[0.02] rounded-lg p-4">
303-
<p className="text-2xl font-bold text-white">
304-
{Math.round(
305-
stats.volume.totalCompleted /
306-
Math.max(stats.escrows.completed, 1)
307-
).toLocaleString()}
308-
</p>
257+
<p className="text-2xl font-bold text-white">{Math.round(stats.volume.totalCompleted / Math.max(stats.escrows.completed, 1)).toLocaleString()}</p>
309258
<p className="text-xs text-gray-500 mt-1">Avg Volume (XLM)</p>
310259
</div>
311260
</div>

0 commit comments

Comments
 (0)