first commit
Deploy / lint (push) Failing after 7s
Deploy / test (push) Has been skipped
Deploy / deploy (push) Has been skipped

This commit is contained in:
Dennis Thiessen
2026-02-20 17:31:01 +01:00
commit 61ab24490d
160 changed files with 17034 additions and 0 deletions
@@ -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>
);
}