feat: add MostActiveStocks table component with auto-refresh
This commit is contained in:
@@ -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<MostActiveStock[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(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 (
|
||||
<div className="bg-white rounded-xl shadow-lg p-6 border border-gray-200">
|
||||
<div className="space-y-3">
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
<div key={i} className="animate-pulse flex gap-4 py-3 border-b border-gray-100 last:border-0">
|
||||
<div className="h-5 bg-gray-200 rounded w-16" />
|
||||
<div className="h-5 bg-gray-200 rounded w-32" />
|
||||
<div className="h-5 bg-gray-200 rounded w-20" />
|
||||
<div className="h-5 bg-gray-200 rounded w-20" />
|
||||
<div className="h-5 bg-gray-200 rounded w-24" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error && stocks.length === 0) {
|
||||
return (
|
||||
<div className="bg-white rounded-xl shadow-lg p-6 border border-gray-200">
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||
<p className="text-red-600 text-sm mb-3">{error}</p>
|
||||
<button
|
||||
onClick={fetchData}
|
||||
className="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-xl shadow-lg border border-gray-200 overflow-hidden">
|
||||
{error && (
|
||||
<div className="bg-red-50 border-b border-red-200 px-6 py-3">
|
||||
<p className="text-red-600 text-sm">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="bg-gray-50 border-b border-gray-200">
|
||||
<th className="text-left px-6 py-4 text-sm font-semibold text-gray-600">Symbol</th>
|
||||
<th className="text-left px-6 py-4 text-sm font-semibold text-gray-600">Name</th>
|
||||
<th className="text-right px-6 py-4 text-sm font-semibold text-gray-600">Price</th>
|
||||
<th className="text-right px-6 py-4 text-sm font-semibold text-gray-600">Change %</th>
|
||||
<th className="text-right px-6 py-4 text-sm font-semibold text-gray-600">Volume</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{stocks.map((stock) => (
|
||||
<tr key={stock.symbol} className="border-b border-gray-100 hover:bg-gray-50 transition-colors">
|
||||
<td className="px-6 py-4">
|
||||
<Link
|
||||
to={`/analyze/${stock.symbol}`}
|
||||
className="text-blue-600 font-semibold hover:text-blue-700 hover:underline"
|
||||
>
|
||||
{stock.symbol}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-gray-600">{stock.name}</td>
|
||||
<td className="px-6 py-4 text-right font-mono text-gray-900">{formatPrice(stock.price)}</td>
|
||||
<td className={`px-6 py-4 text-right font-mono font-medium ${stock.changePercent >= 0 ? "text-green-600" : "text-red-600"}`}>
|
||||
{formatChangePercent(stock.changePercent)}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right text-gray-600">{formatVolume(stock.volume)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user