From 6ff945160d468451dc1cae3952551162edcea3f1 Mon Sep 17 00:00:00 2001 From: Henry Winkel Date: Sat, 16 May 2026 12:45:16 +0200 Subject: [PATCH] feat: add MostActiveStocks table component with auto-refresh --- app/components/MostActiveStocks.tsx | 125 ++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 app/components/MostActiveStocks.tsx diff --git a/app/components/MostActiveStocks.tsx b/app/components/MostActiveStocks.tsx new file mode 100644 index 0000000..0da625f --- /dev/null +++ b/app/components/MostActiveStocks.tsx @@ -0,0 +1,125 @@ +import { useState, useEffect, useCallback } from "react"; +import { Link } from "react-router"; +import type { MostActiveStock } from "../types"; + +function formatVolume(vol: number): string { + if (vol >= 1_000_000_000) return `${(vol / 1_000_000_000).toFixed(1)}B`; + if (vol >= 1_000_000) return `${(vol / 1_000_000).toFixed(1)}M`; + if (vol >= 1_000) return `${(vol / 1_000).toFixed(1)}K`; + return vol.toString(); +} + +function formatPrice(price: number): string { + return `$${price.toFixed(2)}`; +} + +function formatChangePercent(pct: number): string { + return `${pct >= 0 ? "+" : ""}${pct.toFixed(2)}%`; +} + +export default function MostActiveStocks() { + const [stocks, setStocks] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setError(null); + const res = await fetch("/api/stocks/most-actives"); + if (!res.ok) { + const data = await res.json(); + throw new Error(data.error || "Failed to fetch data"); + } + const data = await res.json(); + setStocks(data); + } catch (err) { + const message = err instanceof Error ? err.message : "Failed to fetch most active stocks."; + setError(message); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + const interval = setInterval(fetchData, 30000); + return () => clearInterval(interval); + }, [fetchData]); + + if (loading) { + return ( +
+
+ {Array.from({ length: 8 }).map((_, i) => ( +
+
+
+
+
+
+
+ ))} +
+
+ ); + } + + if (error && stocks.length === 0) { + return ( +
+
+

{error}

+ +
+
+ ); + } + + return ( +
+ {error && ( +
+

{error}

+
+ )} +
+ + + + + + + + + + + + {stocks.map((stock) => ( + + + + + + + + ))} + +
SymbolNamePriceChange %Volume
+ + {stock.symbol} + + {stock.name}{formatPrice(stock.price)}= 0 ? "text-green-600" : "text-red-600"}`}> + {formatChangePercent(stock.changePercent)} + {formatVolume(stock.volume)}
+
+
+ ); +}