Skip to content

Commit 611eefb

Browse files
authored
Merge pull request #316 from Aesdecodes/feature/analytics-skeletons
ux: Implement high-fidelity skeleton loaders for Analytics Dashboard (#65)
2 parents bf5fcad + e073ae5 commit 611eefb

6 files changed

Lines changed: 104 additions & 13 deletions

File tree

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import React from "react";
2+
import SkeletonCard from "./ui/SkeletonCard";
3+
4+
/**
5+
* AnalyticsSkeleton renders a structured placeholder layout for the dashboard.
6+
* Designed to be shown when 'isLoading' is true to prevent layout shifting.
7+
*/
8+
const AnalyticsSkeleton: React.FC = () => {
9+
return (
10+
<div className="space-y-8">
11+
{/* Top Metrics Grid (matching typical 4-column stat layout) */}
12+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
13+
{[...Array(4)].map((_, i) => (
14+
<SkeletonCard key={i} height="h-32" className="p-6 flex flex-col gap-3">
15+
{/* Title placeholder */}
16+
<div className="h-4 w-24 bg-slate-700/50 rounded" />
17+
{/* Value placeholder */}
18+
<div className="h-8 w-32 bg-slate-700/50 rounded" />
19+
{/* Trend placeholder */}
20+
<div className="h-3 w-16 bg-slate-700/50 rounded" />
21+
</SkeletonCard>
22+
))}
23+
</div>
24+
25+
{/* Main Analytics Charts Section */}
26+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
27+
{/* Large Primary Chart (e.g., Volume over time) */}
28+
<SkeletonCard className="lg:col-span-2 p-6" height="h-[400px]">
29+
<div className="flex justify-between items-center mb-8">
30+
<div className="h-6 w-48 bg-slate-700/50 rounded" />
31+
<div className="h-8 w-24 bg-slate-700/50 rounded-lg" />
32+
</div>
33+
{/* Simulated Chart Area */}
34+
<div className="w-full h-64 border-l border-b border-slate-700/50 relative">
35+
<div className="absolute bottom-4 left-4 right-4 h-32 bg-gradient-to-t from-blue-500/10 to-transparent rounded-t-lg" />
36+
</div>
37+
</SkeletonCard>
38+
39+
{/* Secondary Panel (e.g., Asset distribution) */}
40+
<SkeletonCard height="h-[400px]" className="p-6 flex flex-col items-center justify-center">
41+
<div className="h-6 w-32 bg-slate-700/50 rounded mb-8 self-start" />
42+
{/* Simulated Donut Chart */}
43+
<div className="w-48 h-48 rounded-full border-[16px] border-slate-700/50" />
44+
<div className="mt-8 grid grid-cols-2 gap-4 w-full">
45+
<div className="h-3 bg-slate-700/50 rounded" />
46+
<div className="h-3 bg-slate-700/50 rounded" />
47+
</div>
48+
</SkeletonCard>
49+
</div>
50+
51+
{/* Bottom Table/Activity Section */}
52+
<div className="space-y-4">
53+
<div className="h-6 w-48 bg-slate-800/50 animate-pulse rounded" />
54+
<SkeletonCard height="h-64" className="divide-y divide-slate-700/30">
55+
{[...Array(5)].map((_, i) => (
56+
<div key={i} className="h-12 w-full bg-transparent" />
57+
))}
58+
</SkeletonCard>
59+
</div>
60+
</div>
61+
);
62+
};
63+
export default AnalyticsSkeleton;

src/components/DynamicRiskAssessmentChart.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22

33
import dynamic from "next/dynamic";
44
import { Suspense } from "react";
5+
import SkeletonCard from "./ui/SkeletonCard";
56

67
// Loading component
78
const ChartLoadingSkeleton = () => (
8-
<div className="bg-slate-800/50 rounded-xl border border-slate-700/50 p-6">
9+
<SkeletonCard height="h-[520px]" className="p-6">
910
<div className="flex items-center justify-between mb-6">
1011
<div className="flex items-center gap-2">
1112
<div className="w-3 h-3 rounded-full bg-blue-400 animate-pulse"></div>
@@ -29,7 +30,7 @@ const ChartLoadingSkeleton = () => (
2930
</div>
3031
))}
3132
</div>
32-
</div>
33+
</SkeletonCard>
3334
);
3435

3536
// Dynamic import with SSR disabled

src/components/InvoiceTable.tsx

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,8 @@ const InvoiceTable: React.FC<InvoiceTableProps> = ({ filters }) => {
6161
} = useQuery<InvoicesResponse>({
6262
queryKey: ['invoices', currentPage, itemsPerPage, filters],
6363
queryFn: async () => {
64-
const response = await fetch(`/api/invoices?${queryString}`);
65-
if (!response.ok) {
66-
throw new Error('Failed to fetch invoices');
6764
try {
68-
const response = await fetch(
69-
`/api/invoices?page=${currentPage}&limit=${itemsPerPage}`
70-
);
65+
const response = await fetch(`/api/invoices?${queryString}`);
7166
if (!response.ok) {
7267
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
7368
}

src/components/PortfolioChart.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
ResponsiveContainer,
1212
TooltipProps
1313
} from "recharts";
14+
import SkeletonCard from "./ui/SkeletonCard";
1415
import { api } from "../lib/api";
1516

1617
interface PnLData {
@@ -110,9 +111,7 @@ export default function PortfolioChart() {
110111

111112
if (loading) {
112113
return (
113-
<div className="h-64 flex items-center justify-center">
114-
<div className="animate-pulse text-tradeflow-muted">Loading chart data...</div>
115-
</div>
114+
<SkeletonCard height="h-[320px]" className="p-6" />
116115
);
117116
}
118117

src/components/ProModeSection.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,18 @@ import dynamic from "next/dynamic";
55
import Toggle from "../app/Toggle";
66
import { useTokenStore } from "../stores/tokenStore";
77
import PremiumUnlockModal from "./PremiumUnlockModal";
8+
import SkeletonCard from "./ui/SkeletonCard";
89

910
// Dynamically import the heavy chart component with loading fallback
1011
const LivePriceChart = dynamic(() => import("../components/LivePriceChart"), {
1112
ssr: false,
1213
loading: () => (
13-
<div className="w-full h-96 bg-slate-800/50 rounded-xl border border-slate-700/50 flex items-center justify-center">
14+
<SkeletonCard height="h-96" className="flex items-center justify-center">
1415
<div className="text-center">
1516
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto mb-3"></div>
1617
<p className="text-slate-400 text-sm">Loading Pro Chart...</p>
1718
</div>
18-
</div>
19+
</SkeletonCard>
1920
),
2021
});
2122

src/components/ui/SkeletonCard.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import React from "react";
2+
3+
interface SkeletonCardProps {
4+
className?: string;
5+
children?: React.ReactNode;
6+
/**
7+
* Specific height to match the final component and prevent layout shift.
8+
* Use values like 'h-32' for stats or 'h-[400px]' for charts.
9+
*/
10+
height?: string;
11+
}
12+
13+
/**
14+
* SkeletonCard provides a pulsing placeholder for dashboard metrics and charts.
15+
* Complies with requirement #65 using animate-pulse and bg-slate-800.
16+
*/
17+
const SkeletonCard: React.FC<SkeletonCardProps> = ({
18+
className = "",
19+
children,
20+
height = "h-40"
21+
}) => {
22+
return (
23+
<div
24+
className={`bg-slate-800 animate-pulse rounded-2xl border border-slate-700/50 w-full ${height} ${className}`}
25+
aria-hidden="true"
26+
>
27+
{children}
28+
</div>
29+
);
30+
};
31+
32+
export default SkeletonCard;

0 commit comments

Comments
 (0)