93 lines
3.7 KiB
TypeScript
93 lines
3.7 KiB
TypeScript
import { useState } from 'react';
|
|
import { useTickers, useAddTicker, useDeleteTicker } from '../../hooks/useTickers';
|
|
import { ConfirmDialog } from '../ui/ConfirmDialog';
|
|
import { SkeletonTable } from '../ui/Skeleton';
|
|
import { formatDateTime } from '../../lib/format';
|
|
|
|
export function TickerManagement() {
|
|
const { data: tickers, isLoading, isError, error } = useTickers();
|
|
const addTicker = useAddTicker();
|
|
const deleteTicker = useDeleteTicker();
|
|
const [newSymbol, setNewSymbol] = useState('');
|
|
const [deleteTarget, setDeleteTarget] = useState<string | null>(null);
|
|
|
|
function handleAdd(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
const symbol = newSymbol.trim().toUpperCase();
|
|
if (!symbol) return;
|
|
addTicker.mutate(symbol, { onSuccess: () => setNewSymbol('') });
|
|
}
|
|
|
|
function handleConfirmDelete() {
|
|
if (!deleteTarget) return;
|
|
deleteTicker.mutate(deleteTarget, { onSuccess: () => setDeleteTarget(null) });
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<form onSubmit={handleAdd} className="flex gap-3">
|
|
<input
|
|
type="text"
|
|
value={newSymbol}
|
|
onChange={(e) => setNewSymbol(e.target.value)}
|
|
placeholder="Enter ticker symbol (e.g. AAPL)"
|
|
className="flex-1 input-glass px-3 py-2.5 text-sm"
|
|
/>
|
|
<button
|
|
type="submit"
|
|
disabled={addTicker.isPending || !newSymbol.trim()}
|
|
className="btn-gradient px-4 py-2.5 text-sm disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
<span>{addTicker.isPending ? 'Adding…' : 'Add Ticker'}</span>
|
|
</button>
|
|
</form>
|
|
|
|
{isLoading && <SkeletonTable rows={5} cols={3} />}
|
|
{isError && <p className="text-sm text-red-400">{(error as Error)?.message || 'Failed to load tickers'}</p>}
|
|
|
|
{tickers && tickers.length > 0 && (
|
|
<div className="glass overflow-x-auto">
|
|
<table className="w-full text-sm text-left">
|
|
<thead className="border-b border-white/[0.06] text-gray-500">
|
|
<tr>
|
|
<th className="px-4 py-3 font-medium text-xs uppercase tracking-wider">Symbol</th>
|
|
<th className="px-4 py-3 font-medium text-xs uppercase tracking-wider">Added</th>
|
|
<th className="px-4 py-3 font-medium text-xs uppercase tracking-wider text-right">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-white/[0.04]">
|
|
{tickers.map((ticker) => (
|
|
<tr key={ticker.id} className="hover:bg-white/[0.03] transition-all duration-150">
|
|
<td className="px-4 py-3 font-medium text-gray-100">{ticker.symbol}</td>
|
|
<td className="px-4 py-3 text-gray-400">{formatDateTime(ticker.created_at)}</td>
|
|
<td className="px-4 py-3 text-right">
|
|
<button
|
|
onClick={() => setDeleteTarget(ticker.symbol)}
|
|
disabled={deleteTicker.isPending}
|
|
className="rounded-lg border border-red-500/20 bg-red-500/10 px-3 py-1 text-xs text-red-400 hover:bg-red-500/20 disabled:opacity-50 transition-all duration-200"
|
|
>
|
|
Delete
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
|
|
{tickers && tickers.length === 0 && (
|
|
<p className="text-sm text-gray-500">No tickers registered yet. Add one above.</p>
|
|
)}
|
|
|
|
<ConfirmDialog
|
|
open={deleteTarget !== null}
|
|
title="Delete Ticker"
|
|
message={`Are you sure you want to delete ${deleteTarget}? This action cannot be undone.`}
|
|
onConfirm={handleConfirmDelete}
|
|
onCancel={() => setDeleteTarget(null)}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|