From 0fb1d63cbefa0962b4cfa414b4b7430273bd6e75 Mon Sep 17 00:00:00 2001 From: Saravana Achu Mac Date: Mon, 4 May 2026 16:38:17 -0700 Subject: [PATCH] fix(D1): add dashboard skeleton loaders Replace plain loading text in news, research cards, and screener results with stable skeleton layouts so the dashboard holds its shape while market data requests resolve. Refs: docs/AUDIT_REDESIGN.md item D1. Co-Authored-By: GPT-5 Codex --- web/src/components/Skeleton.tsx | 33 ++++++++++++++++++++ web/src/components/layout/RightPanel.tsx | 32 ++++++++++++++++--- web/src/views/HomeView.tsx | 19 +++++++++--- web/src/views/ScreenerView.tsx | 39 ++++++++++++++++++------ 4 files changed, 105 insertions(+), 18 deletions(-) create mode 100644 web/src/components/Skeleton.tsx diff --git a/web/src/components/Skeleton.tsx b/web/src/components/Skeleton.tsx new file mode 100644 index 0000000..1e38a04 --- /dev/null +++ b/web/src/components/Skeleton.tsx @@ -0,0 +1,33 @@ +import type { CSSProperties } from 'react'; + +interface SkeletonBlockProps { + width?: number | string; + height?: number | string; + radius?: number; + style?: CSSProperties; +} + +export function SkeletonBlock({ + width = '100%', + height = 12, + radius = 999, + style, +}: SkeletonBlockProps) { + return ( + - {loading && ( -
Loading news…
- )} + {loading && } {!loading && error && (
{error}
@@ -194,7 +218,7 @@ function NewsFeed() { )} - {news.map((a, i) => )} + {!loading && news.map((a, i) => )} ); } diff --git a/web/src/views/HomeView.tsx b/web/src/views/HomeView.tsx index 4f063b5..a750d7a 100644 --- a/web/src/views/HomeView.tsx +++ b/web/src/views/HomeView.tsx @@ -10,6 +10,7 @@ import { fetchChartBars, fetchResearchProfile, fetchResearchMetrics, fetchResearchEarnings, type OHLCVBar, } from '../lib/marketApi'; +import { SkeletonBlock, SkeletonText } from '../components/Skeleton'; // ─── Time period config ─────────────────────────────────────────────────────── const PERIODS = ['1D', '5D', '1M', '3M', '6M', 'YTD', '1Y', '5Y', 'MAX'] as const; @@ -589,6 +590,7 @@ function ResearchCards({ const nextEarnings = earnings.find(e => e.date && new Date(e.date) >= new Date()); const fmtDate = (d?: string) => d ? new Date(d).toLocaleDateString('en-US', { month:'short', day:'numeric', year:'numeric' }) : '—'; + const ValueSkeleton = ({ width = 58 }: { width?: number }) => ; const financialRows: [string, string][] = [ ['Market Cap', fmtBig(profile?.mktCap)], @@ -610,7 +612,12 @@ function ResearchCards({ 📋 Company {profileLoading ? ( -
Loading…
+
+ + + + +
) : profile ? ( <>
@@ -646,7 +653,9 @@ function ResearchCards({ padding: '5px 0', borderBottom: '1px solid #F9FAFB', }}> {label} - {loading || profileLoading ? '…' : val} + + {loading || profileLoading ? 10 ? 64 : 46} /> : val} +
))} @@ -667,10 +676,12 @@ function ResearchCards({ padding: '5px 0', borderBottom: '1px solid #F9FAFB', }}> {label} - {val} + + {val === '…' ? : val} + ))} - {earnings.length > 0 && ( + {!loading && earnings.length > 0 && (
Past Earnings
{earnings.slice(0,3).map((e, i) => ( diff --git a/web/src/views/ScreenerView.tsx b/web/src/views/ScreenerView.tsx index ff70aaf..a176f5b 100644 --- a/web/src/views/ScreenerView.tsx +++ b/web/src/views/ScreenerView.tsx @@ -1,10 +1,11 @@ import { useState, useEffect, useCallback } from 'react'; -import { SlidersHorizontal, Search, Loader2, RefreshCw } from 'lucide-react'; +import { SlidersHorizontal, Search, RefreshCw } from 'lucide-react'; import { useAppContext } from '../context/AppContext'; import { useNavigate } from 'react-router-dom'; import { getPlatformAccessToken } from '../lib/authSession'; import { tradingRuntime } from '../lib/runtime'; import { createRequestId } from '../../../shared/request-id.js'; +import { SkeletonBlock } from '../components/Skeleton'; // ─── Types ──────────────────────────────────────────────────────────────────── interface ScreenerRow { @@ -126,6 +127,32 @@ export function ScreenerView() { ); + const ScreenerSkeletonRows = () => ( +
+ {Array.from({ length: 6 }).map((_, i) => ( +
+ + + + + + + +
+ ))} +
+ ); + const handleRowClick = (symbol: string) => { setActiveSymbol(symbol); navigate('/'); @@ -271,15 +298,7 @@ export function ScreenerView() {
{/* Loading */} - {loading && ( -
- - Fetching screener results… -
- )} + {loading && } {/* Rows */} {!loading && filtered.map((row, i) => (