phase 16: React dashboard with full platform control and analytics
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user