Skip to content

Commit aa21698

Browse files
authored
feat(ui): replace spinner with skeleton loading screen on dashboard (#5)
* feat(ui): replace spinner with skeleton loading screen on dashboard The dashboard previously showed a bare spinner + "Loading results..." text while waiting for the /api/results fetch. This gave no visual hint of page structure, causing layout shift when content arrived. Replaced with a full DashboardSkeleton component that mirrors the real layout: - Four stat cards (icon, value, sub-label placeholders) - Two chart panels with proportional bar columns - Recent Results table with six skeleton rows Uses the existing .skeleton shimmer CSS class (globals.css) — no new dependencies. The skeleton renders immediately on mount so the page always has visible content during the loading state. Also wires aria-busy='true' and aria-label on the skeleton root so screen readers know data is loading. Files: web/src/components/DashboardSkeleton.tsx (new), web/src/app/dashboard/page.tsx * docs: update IMPROVEMENTS.md for 2026-03-18 UI/UX skeleton --------- Co-authored-by: bokiko <bokiko@users.noreply.github.com>
1 parent 3dc738a commit aa21698

3 files changed

Lines changed: 137 additions & 4 deletions

File tree

IMPROVEMENTS.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,17 @@ and work offline. 68 tests across 10 test classes covering:
5858
**Files changed:** `desktop/tests/__init__.py`, `desktop/tests/test_ping_tester.py`,
5959
`desktop/tests/test_cli.py`, `desktop/pytest.ini`
6060
**Lines:** +370 / -0
61+
62+
## 2026-03-18 — UI/UX: Skeleton loading screen for dashboard
63+
64+
The dashboard previously showed a centered spinner while fetching results from
65+
/api/results, giving no visual indication of page structure and causing a jarring
66+
layout shift when content arrived.
67+
68+
Replaced the spinner with a DashboardSkeleton component that mirrors the real
69+
dashboard layout — four stat cards, two chart panels, and a six-row results table
70+
— all with the existing .skeleton shimmer animation. No new dependencies. Screen
71+
reader support via aria-busy="true" and aria-label on the skeleton root.
72+
73+
**Files changed:** `web/src/components/DashboardSkeleton.tsx` (new), `web/src/app/dashboard/page.tsx`
74+
**Lines:** +123 / -4

web/src/app/dashboard/page.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
} from "lucide-react";
1515
import { Navbar } from "@/components/Navbar";
1616
import { Footer } from "@/components/Footer";
17+
import { DashboardSkeleton } from "@/components/DashboardSkeleton";
1718
import {
1819
LineChart,
1920
Line,
@@ -181,10 +182,7 @@ export default function DashboardPage() {
181182
</div>
182183

183184
{loading ? (
184-
<div className="text-center py-20">
185-
<div className="animate-spin w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full mx-auto mb-4"></div>
186-
<p className="text-zinc-400">Loading results...</p>
187-
</div>
185+
<DashboardSkeleton />
188186
) : error ? (
189187
<div className="text-center py-20">
190188
<AlertCircle className="w-16 h-16 text-red-500 mx-auto mb-4" />
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/**
2+
* DashboardSkeleton — shimmer placeholders that mirror the real dashboard layout.
3+
*
4+
* Renders four stat cards, two chart panels, and a results table skeleton so the
5+
* page feels instantly populated while data is in-flight. Uses a CSS animation
6+
* defined in globals.css (shimmer keyframes) to avoid a runtime dependency.
7+
*/
8+
9+
function SkeletonBlock({ className = "" }: { className?: string }) {
10+
return (
11+
<div
12+
className={`skeleton ${className}`}
13+
aria-hidden="true"
14+
/>
15+
);
16+
}
17+
18+
function StatCardSkeleton() {
19+
return (
20+
<div className="bg-zinc-900 border border-zinc-800 rounded-xl p-6">
21+
{/* Icon + label row */}
22+
<div className="flex items-center gap-3 mb-3">
23+
<SkeletonBlock className="w-5 h-5 rounded-full" />
24+
<SkeletonBlock className="h-3 w-24" />
25+
</div>
26+
{/* Value */}
27+
<SkeletonBlock className="h-9 w-20 mb-2" />
28+
{/* Sub-label */}
29+
<SkeletonBlock className="h-3 w-16" />
30+
</div>
31+
);
32+
}
33+
34+
function ChartPanelSkeleton() {
35+
return (
36+
<div className="bg-zinc-900 border border-zinc-800 rounded-xl p-6">
37+
{/* Title */}
38+
<SkeletonBlock className="h-5 w-36 mb-5" />
39+
{/* Chart area — fake bar/line columns */}
40+
<div className="h-64 flex items-end gap-2 px-2">
41+
{[45, 70, 55, 85, 40, 65, 75, 50, 90, 60].map((h, i) => (
42+
<div
43+
key={i}
44+
className="skeleton flex-1 rounded-t"
45+
style={{ height: `${h}%` }}
46+
aria-hidden="true"
47+
/>
48+
))}
49+
</div>
50+
</div>
51+
);
52+
}
53+
54+
function TableRowSkeleton() {
55+
return (
56+
<tr className="border-t border-zinc-800">
57+
<td className="py-4">
58+
<SkeletonBlock className="h-4 w-32 mb-1.5" />
59+
<SkeletonBlock className="h-3 w-16" />
60+
</td>
61+
<td className="py-4">
62+
<SkeletonBlock className="h-4 w-14" />
63+
</td>
64+
<td className="py-4">
65+
<SkeletonBlock className="h-4 w-12" />
66+
</td>
67+
<td className="py-4">
68+
<SkeletonBlock className="h-4 w-10" />
69+
</td>
70+
<td className="py-4">
71+
<SkeletonBlock className="h-4 w-24" />
72+
</td>
73+
<td className="py-4">
74+
<SkeletonBlock className="h-4 w-20" />
75+
</td>
76+
</tr>
77+
);
78+
}
79+
80+
export function DashboardSkeleton() {
81+
return (
82+
<div aria-busy="true" aria-label="Loading dashboard data">
83+
{/* Stat Cards */}
84+
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
85+
{[0, 1, 2, 3].map((i) => (
86+
<StatCardSkeleton key={i} />
87+
))}
88+
</div>
89+
90+
{/* Charts */}
91+
<div className="grid md:grid-cols-2 gap-6 mb-8">
92+
<ChartPanelSkeleton />
93+
<ChartPanelSkeleton />
94+
</div>
95+
96+
{/* Recent Results Table */}
97+
<div className="bg-zinc-900 border border-zinc-800 rounded-xl p-6">
98+
{/* Table title */}
99+
<SkeletonBlock className="h-5 w-32 mb-5" />
100+
<div className="overflow-x-auto">
101+
<table className="w-full">
102+
<thead>
103+
<tr className="text-left">
104+
{["Server", "Ping", "Jitter", "Loss", "ISP", "Time"].map((col) => (
105+
<th key={col} className="pb-4">
106+
<SkeletonBlock className="h-3 w-14" />
107+
</th>
108+
))}
109+
</tr>
110+
</thead>
111+
<tbody>
112+
{[0, 1, 2, 3, 4, 5].map((i) => (
113+
<TableRowSkeleton key={i} />
114+
))}
115+
</tbody>
116+
</table>
117+
</div>
118+
</div>
119+
</div>
120+
);
121+
}

0 commit comments

Comments
 (0)