phase 16: React dashboard with full platform control and analytics

This commit is contained in:
Celes Renata
2026-04-11 16:19:46 -07:00
parent 25e0e386b7
commit faccb0b8db
53 changed files with 7924 additions and 13 deletions
+82
View File
@@ -0,0 +1,82 @@
import { useState } from 'react';
import { useWatchlists, useWatchlistMembers, useCreateWatchlist } from '../api/hooks';
import { LoadingSpinner, Card } from '../components/ui';
export function WatchlistsPage() {
const { data: watchlists, isLoading } = useWatchlists();
const [selected, setSelected] = useState<string | null>(null);
const [showCreate, setShowCreate] = useState(false);
if (isLoading) return <LoadingSpinner />;
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h1 className="text-xl font-semibold text-gray-100">Watchlists</h1>
<button onClick={() => setShowCreate(true)} className="rounded-md bg-brand-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-brand-700">
New Watchlist
</button>
</div>
{showCreate && <CreateWatchlistForm onClose={() => setShowCreate(false)} />}
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{(watchlists ?? []).map((wl) => (
<Card
key={wl.id}
className={`cursor-pointer transition-colors hover:border-brand-500/50 ${selected === wl.id ? 'border-brand-500' : ''}`}
>
<button className="w-full text-left" onClick={() => setSelected(selected === wl.id ? null : wl.id)}>
<div className="font-medium text-gray-200">{wl.name}</div>
{wl.description && <div className="mt-1 text-xs text-gray-500">{wl.description}</div>}
</button>
</Card>
))}
</div>
{selected && <WatchlistMembers watchlistId={selected} />}
</div>
);
}
function WatchlistMembers({ watchlistId }: { watchlistId: string }) {
const { data: members, isLoading } = useWatchlistMembers(watchlistId);
if (isLoading) return <LoadingSpinner />;
if (!members?.length) return <p className="text-sm text-gray-500">No members in this watchlist</p>;
return (
<Card>
<h2 className="mb-2 text-sm font-medium text-gray-400">Members</h2>
<div className="flex flex-wrap gap-2">
{members.map((m) => (
<span key={m.id} className="rounded-full border border-surface-700 bg-surface-800 px-3 py-1 text-xs text-gray-300">
{m.ticker} {m.legal_name}
</span>
))}
</div>
</Card>
);
}
function CreateWatchlistForm({ onClose }: { onClose: () => void }) {
const [name, setName] = useState('');
const [desc, setDesc] = useState('');
const mutation = useCreateWatchlist();
return (
<Card>
<form
className="flex gap-2"
onSubmit={(e) => {
e.preventDefault();
if (name.trim()) mutation.mutate({ name: name.trim(), description: desc || undefined }, { onSuccess: onClose });
}}
>
<input type="text" placeholder="Name" value={name} onChange={(e) => setName(e.target.value)} required className="rounded-md border border-surface-700 bg-surface-900 px-3 py-1.5 text-sm text-gray-200 placeholder-gray-500 focus:border-brand-500 focus:outline-none" aria-label="Watchlist name" />
<input type="text" placeholder="Description (optional)" value={desc} onChange={(e) => setDesc(e.target.value)} className="flex-1 rounded-md border border-surface-700 bg-surface-900 px-3 py-1.5 text-sm text-gray-200 placeholder-gray-500 focus:border-brand-500 focus:outline-none" aria-label="Watchlist description" />
<button type="submit" disabled={mutation.isPending} className="rounded-md bg-brand-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-brand-700 disabled:opacity-50">Create</button>
<button type="button" onClick={onClose} className="rounded-md border border-surface-700 px-3 py-1.5 text-sm text-gray-400 hover:bg-surface-800">Cancel</button>
</form>
</Card>
);
}