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) => (