first commit
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
import { FormEvent, useState } from 'react';
|
||||
import { useAddToWatchlist } from '../../hooks/useWatchlist';
|
||||
|
||||
export function AddTickerForm() {
|
||||
const [symbol, setSymbol] = useState('');
|
||||
const addMutation = useAddToWatchlist();
|
||||
|
||||
function handleSubmit(e: FormEvent) {
|
||||
e.preventDefault();
|
||||
const trimmed = symbol.trim().toUpperCase();
|
||||
if (!trimmed) return;
|
||||
addMutation.mutate(trimmed, { onSuccess: () => setSymbol('') });
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={symbol}
|
||||
onChange={(e) => setSymbol(e.target.value)}
|
||||
placeholder="Add symbol (e.g. AAPL)"
|
||||
className="input-glass px-3 py-2 text-sm"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={addMutation.isPending || !symbol.trim()}
|
||||
className="btn-gradient px-4 py-2 text-sm disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<span>{addMutation.isPending ? 'Adding…' : 'Add'}</span>
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import type { WatchlistEntry } from '../../lib/types';
|
||||
import { formatPrice } from '../../lib/format';
|
||||
import { Badge } from '../ui/Badge';
|
||||
import { useRemoveFromWatchlist } from '../../hooks/useWatchlist';
|
||||
|
||||
function scoreColor(score: number): string {
|
||||
if (score > 70) return 'text-emerald-400';
|
||||
if (score >= 40) return 'text-amber-400';
|
||||
return 'text-red-400';
|
||||
}
|
||||
|
||||
interface WatchlistTableProps {
|
||||
entries: WatchlistEntry[];
|
||||
}
|
||||
|
||||
export function WatchlistTable({ entries }: WatchlistTableProps) {
|
||||
const removeMutation = useRemoveFromWatchlist();
|
||||
|
||||
if (entries.length === 0) {
|
||||
return (
|
||||
<p className="py-8 text-center text-sm text-gray-500">
|
||||
No watchlist entries yet. Add a symbol above to get started.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="glass overflow-x-auto">
|
||||
<table className="w-full text-left text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-white/[0.06] text-xs uppercase tracking-wider text-gray-500">
|
||||
<th className="px-4 py-3">Symbol</th>
|
||||
<th className="px-4 py-3">Type</th>
|
||||
<th className="px-4 py-3">Score</th>
|
||||
<th className="px-4 py-3">Dimensions</th>
|
||||
<th className="px-4 py-3">R:R</th>
|
||||
<th className="px-4 py-3">Direction</th>
|
||||
<th className="px-4 py-3">S/R Levels</th>
|
||||
<th className="px-4 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{entries.map((entry) => (
|
||||
<tr
|
||||
key={entry.symbol}
|
||||
className="border-b border-white/[0.04] transition-all duration-200 hover:bg-white/[0.03]"
|
||||
>
|
||||
<td className="px-4 py-3.5">
|
||||
<Link
|
||||
to={`/ticker/${entry.symbol}`}
|
||||
className="font-medium text-blue-400 hover:text-blue-300 transition-colors duration-150"
|
||||
>
|
||||
{entry.symbol}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="px-4 py-3.5">
|
||||
<Badge label={entry.entry_type} variant={entry.entry_type === 'auto' ? 'auto' : 'manual'} />
|
||||
</td>
|
||||
<td className="px-4 py-3.5">
|
||||
{entry.composite_score !== null ? (
|
||||
<span className={`font-semibold ${scoreColor(entry.composite_score)}`}>
|
||||
{Math.round(entry.composite_score)}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-gray-500">—</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3.5">
|
||||
{entry.dimensions.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{entry.dimensions.map((d) => (
|
||||
<span
|
||||
key={d.dimension}
|
||||
className={`inline-block rounded-md px-1.5 py-0.5 text-xs bg-white/[0.04] ${scoreColor(d.score)}`}
|
||||
title={d.dimension}
|
||||
>
|
||||
{d.dimension.slice(0, 3).toUpperCase()} {Math.round(d.score)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-gray-500">—</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3.5 font-mono">
|
||||
{entry.rr_ratio !== null ? (
|
||||
<span className="text-gray-200">{entry.rr_ratio.toFixed(2)}</span>
|
||||
) : (
|
||||
<span className="text-gray-500">—</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3.5">
|
||||
{entry.rr_direction ? (
|
||||
<span className={entry.rr_direction === 'long' ? 'text-emerald-400' : entry.rr_direction === 'short' ? 'text-red-400' : 'text-gray-400'}>
|
||||
{entry.rr_direction}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-gray-500">—</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3.5">
|
||||
{entry.sr_levels.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{entry.sr_levels.map((level, i) => (
|
||||
<span key={i} className={`text-xs ${level.type === 'support' ? 'text-emerald-400' : 'text-red-400'}`}>
|
||||
{formatPrice(level.price_level)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-gray-500">—</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3.5">
|
||||
<button
|
||||
onClick={() => removeMutation.mutate(entry.symbol)}
|
||||
disabled={removeMutation.isPending}
|
||||
className="rounded-lg px-2.5 py-1 text-xs text-red-400 transition-all duration-150 hover:bg-red-500/10 hover:text-red-300 disabled:opacity-50"
|
||||
aria-label={`Remove ${entry.symbol}`}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user