mirror of
https://github.com/Open-Dev-Society/OpenStock.git
synced 2026-05-06 21:50:16 +08:00
169 lines
9.2 KiB
TypeScript
169 lines
9.2 KiB
TypeScript
"use client";
|
|
|
|
import React, { useEffect, useState } from "react";
|
|
import Image from "next/image";
|
|
import Link from "next/link";
|
|
import { ArrowUp, ArrowDown, Bell } from "lucide-react";
|
|
import CreateAlertModal from "./CreateAlertModal";
|
|
import WatchlistButton from "@/components/WatchlistButton";
|
|
import { formatCurrency, formatNumber } from "@/lib/utils";
|
|
import { removeFromWatchlist } from "@/lib/actions/watchlist.actions";
|
|
|
|
interface WatchlistTableProps {
|
|
data: any[];
|
|
userId: string;
|
|
onRefresh?: () => void;
|
|
}
|
|
|
|
export default function WatchlistTable({ data, userId, onRefresh }: WatchlistTableProps) {
|
|
const [stocks, setStocks] = useState(data);
|
|
|
|
useEffect(() => {
|
|
// Initial set if prop changes
|
|
setStocks(data);
|
|
}, [data]);
|
|
|
|
useEffect(() => {
|
|
if (!stocks || stocks.length === 0) return;
|
|
|
|
// Poll for price updates every 15 seconds
|
|
const interval = setInterval(async () => {
|
|
try {
|
|
const symbols = stocks.map(s => s.symbol);
|
|
if (symbols.length === 0) return;
|
|
|
|
// Dynamic import to avoid server-action issues if directly imported in client component sometimes
|
|
const { getWatchlistData } = await import('@/lib/actions/finnhub.actions');
|
|
const updatedData = await getWatchlistData(symbols);
|
|
|
|
if (updatedData && updatedData.length > 0) {
|
|
setStocks(current => {
|
|
const map = new Map(updatedData.map(item => [item.symbol, item]));
|
|
return current.map(existing => {
|
|
const fresh = map.get(existing.symbol);
|
|
if (fresh) {
|
|
return {
|
|
...existing,
|
|
price: fresh.price,
|
|
change: fresh.change,
|
|
changePercent: fresh.changePercent,
|
|
};
|
|
}
|
|
return existing;
|
|
});
|
|
});
|
|
}
|
|
} catch (err) {
|
|
console.error("Failed to poll watchlist prices", err);
|
|
}
|
|
}, 5000);
|
|
|
|
return () => clearInterval(interval);
|
|
}, [stocks]); // Re-create interval if list size changes
|
|
|
|
if (!stocks || stocks.length === 0) {
|
|
return (
|
|
<div className="text-center py-12 bg-gray-900/50 rounded-lg border border-gray-800">
|
|
<h3 className="text-xl font-medium text-gray-300 mb-2">Your watchlist is empty</h3>
|
|
<p className="text-gray-500 mb-6">Add stocks to track their performance and set alerts.</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="overflow-hidden rounded-xl border border-white/10 bg-black/40 backdrop-blur-md shadow-xl">
|
|
<table className="w-full text-left text-sm border-collapse">
|
|
<thead className="bg-white/5 text-gray-400 font-medium border-b border-white/10">
|
|
<tr>
|
|
<th className="px-6 py-4 font-semibold tracking-wide">Company</th>
|
|
<th className="px-6 py-4 font-semibold tracking-wide">Symbol</th>
|
|
<th className="px-6 py-4 font-semibold tracking-wide">Price</th>
|
|
<th className="px-6 py-4 font-semibold tracking-wide">Change</th>
|
|
<th className="px-6 py-4 font-semibold tracking-wide">Market Cap</th>
|
|
<th className="px-6 py-4 text-right font-semibold tracking-wide">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-white/10">
|
|
{stocks.map((stock: any) => {
|
|
const isPositive = stock.change >= 0;
|
|
return (
|
|
<tr key={stock.symbol} className="hover:bg-white/5 transition-colors group">
|
|
<td className="px-6 py-4">
|
|
<div className="flex items-center space-x-4">
|
|
{stock.logo ? (
|
|
<div className="w-10 h-10 relative rounded-full overflow-hidden bg-white/10 shadow-sm border border-white/5">
|
|
<Image
|
|
src={stock.logo}
|
|
alt={stock.symbol}
|
|
fill
|
|
className="object-contain p-1.5"
|
|
/>
|
|
</div>
|
|
) : (
|
|
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-gray-700 to-gray-800 flex items-center justify-center text-xs font-bold text-white shadow-sm border border-white/5">
|
|
{stock.symbol[0]}
|
|
</div>
|
|
)}
|
|
<div className="flex flex-col">
|
|
<span className="font-semibold text-white text-base">{stock.name}</span>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4 font-medium text-gray-300">
|
|
<span className="bg-white/5 px-2.5 py-1 rounded-md text-xs font-mono border border-white/10">
|
|
{stock.symbol}
|
|
</span>
|
|
</td>
|
|
<td className="px-6 py-4 text-white font-medium text-base tracking-tight">
|
|
{formatCurrency(stock.price)}
|
|
</td>
|
|
<td className={`px-6 py-4 font-medium`}>
|
|
<div className={`flex items-center w-fit px-2 py-1 rounded-md ${isPositive ? "bg-green-500/10 text-green-400" : "bg-red-500/10 text-red-400"}`}>
|
|
{isPositive ? <ArrowUp className="w-3.5 h-3.5 mr-1.5" /> : <ArrowDown className="w-3.5 h-3.5 mr-1.5" />}
|
|
{Math.abs(stock.changePercent).toFixed(2)}%
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4 text-gray-400 font-medium">
|
|
{formatNumber(stock.marketCap)}
|
|
</td>
|
|
<td className="px-6 py-4 text-right">
|
|
<div className="flex items-center justify-end space-x-3 opacity-80 group-hover:opacity-100 transition-opacity">
|
|
<CreateAlertModal
|
|
userId={userId}
|
|
symbol={stock.symbol}
|
|
currentPrice={stock.price}
|
|
onAlertCreated={onRefresh}
|
|
>
|
|
<button className="p-2.5 rounded-full text-gray-400 hover:text-white hover:bg-white/10 transition-all border border-transparent hover:border-white/10" title="Add Alert">
|
|
<Bell className="w-4.5 h-4.5" />
|
|
</button>
|
|
</CreateAlertModal>
|
|
|
|
<div className="transform scale-95 hover:scale-100 transition-transform">
|
|
<WatchlistButton
|
|
symbol={stock.symbol}
|
|
company={stock.name}
|
|
isInWatchlist={true}
|
|
type="icon"
|
|
showTrashIcon={false}
|
|
onWatchlistChange={async (sym, added) => {
|
|
if (!added) {
|
|
await removeFromWatchlist(userId, sym);
|
|
// Update local list faster than full page refresh if you want
|
|
setStocks((curr: any[]) => curr.filter((s: any) => s.symbol !== sym));
|
|
if (onRefresh) onRefresh();
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
);
|
|
}
|