make watchlist fully manual; add price + day-change, two-block overview
Per design decision: the watchlist is now purely user-curated (no auto-seeding of the top-10), so the auto_populate/dismissed machinery is removed and removals are plain deletes. Each entry is enriched with latest close + day-over-day move. Overview now shows two clear blocks: Top Setups (what to trade) and My Watchlist (my names with current price and today's %). Market watchlist table drops the now-meaningless auto/manual Type column in favour of Price and Day columns. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
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 {
|
||||
@@ -10,6 +9,12 @@ function scoreColor(score: number): string {
|
||||
return 'text-red-400';
|
||||
}
|
||||
|
||||
function changeColor(value: number): string {
|
||||
if (value > 0) return 'text-emerald-400';
|
||||
if (value < 0) return 'text-red-400';
|
||||
return 'text-gray-300';
|
||||
}
|
||||
|
||||
interface WatchlistTableProps {
|
||||
entries: WatchlistEntry[];
|
||||
}
|
||||
@@ -31,7 +36,8 @@ export function WatchlistTable({ entries }: WatchlistTableProps) {
|
||||
<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">Price</th>
|
||||
<th className="px-4 py-3">Day</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>
|
||||
@@ -54,8 +60,17 @@ export function WatchlistTable({ entries }: WatchlistTableProps) {
|
||||
{entry.symbol}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="px-4 py-3.5">
|
||||
<Badge label={entry.entry_type} variant={entry.entry_type === 'auto' ? 'auto' : 'manual'} />
|
||||
<td className="px-4 py-3.5 num text-gray-200">
|
||||
{entry.last_close !== null ? formatPrice(entry.last_close) : <span className="text-gray-500">—</span>}
|
||||
</td>
|
||||
<td className="px-4 py-3.5 num">
|
||||
{entry.change_pct !== null ? (
|
||||
<span className={changeColor(entry.change_pct)}>
|
||||
{entry.change_pct >= 0 ? '+' : ''}{entry.change_pct.toFixed(2)}%
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-gray-500">—</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3.5">
|
||||
{entry.composite_score !== null ? (
|
||||
|
||||
@@ -20,6 +20,9 @@ export interface WatchlistEntry {
|
||||
rr_ratio: number | null;
|
||||
rr_direction: string | null;
|
||||
sr_levels: SRLevelSummary[];
|
||||
last_close: number | null;
|
||||
change_pct: number | null;
|
||||
price_date: string | null;
|
||||
added_at: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ export default function DashboardPage() {
|
||||
() =>
|
||||
[...(watchlist.data ?? [])]
|
||||
.sort((a, b) => (b.composite_score ?? -1) - (a.composite_score ?? -1))
|
||||
.slice(0, 6),
|
||||
.slice(0, 10),
|
||||
[watchlist.data],
|
||||
);
|
||||
|
||||
@@ -212,13 +212,15 @@ export default function DashboardPage() {
|
||||
</Section>
|
||||
</div>
|
||||
|
||||
{/* Watchlist pulse */}
|
||||
{/* My watchlist */}
|
||||
<div className="xl:col-span-2">
|
||||
<Section title="Watchlist Pulse" hint="top by score">
|
||||
<Section title="My Watchlist" hint="today's move">
|
||||
{watchlist.isLoading && <SkeletonTable rows={6} cols={3} />}
|
||||
{watchlist.isError && <Callout variant="error">Failed to load watchlist</Callout>}
|
||||
{watchlist.data && topWatchlist.length === 0 && (
|
||||
<Callout variant="empty">Watchlist is empty — add tickers on the Market page.</Callout>
|
||||
<Callout variant="empty">
|
||||
Your watchlist is empty — open any ticker and tap “☆ Add to watchlist”.
|
||||
</Callout>
|
||||
)}
|
||||
{topWatchlist.length > 0 && (
|
||||
<div className="glass overflow-hidden">
|
||||
@@ -229,13 +231,20 @@ export default function DashboardPage() {
|
||||
to={`/ticker/${entry.symbol}`}
|
||||
className="flex items-center justify-between px-4 py-3 transition-colors duration-150 hover:bg-white/[0.03]"
|
||||
>
|
||||
<span className="font-medium text-gray-200">{entry.symbol}</span>
|
||||
<span className="flex items-center gap-4">
|
||||
{entry.rr_ratio != null && (
|
||||
<span className="num text-xs text-gray-500">{entry.rr_ratio.toFixed(1)}:1</span>
|
||||
<span className="flex items-baseline gap-2">
|
||||
<span className="font-medium text-gray-200">{entry.symbol}</span>
|
||||
{entry.composite_score != null && (
|
||||
<span className="num text-[10px] text-gray-500">score {entry.composite_score.toFixed(0)}</span>
|
||||
)}
|
||||
<span className="num text-sm font-semibold text-blue-300">
|
||||
{entry.composite_score != null ? entry.composite_score.toFixed(0) : '—'}
|
||||
</span>
|
||||
<span className="flex items-baseline gap-3">
|
||||
<span className="num text-sm text-gray-200">
|
||||
{entry.last_close != null ? formatPrice(entry.last_close) : '—'}
|
||||
</span>
|
||||
<span className={`num w-16 text-right text-sm font-semibold ${rColor(entry.change_pct)}`}>
|
||||
{entry.change_pct != null
|
||||
? `${entry.change_pct >= 0 ? '+' : ''}${entry.change_pct.toFixed(2)}%`
|
||||
: '—'}
|
||||
</span>
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
Reference in New Issue
Block a user