first commit
This commit is contained in:
92
frontend/src/components/admin/TickerManagement.tsx
Normal file
92
frontend/src/components/admin/TickerManagement.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user