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
+116
View File
@@ -0,0 +1,116 @@
import { useState } from 'react';
import { useNavigate } from '@tanstack/react-router';
import { useTrends } from '../api/hooks';
import { TrendArrow, ConfidenceBar, LoadingSpinner, TickerFilter, Card } from '../components/ui';
import type { TrendSummary } from '../api/hooks';
const WINDOWS = ['intraday', '1d', '7d', '30d', '90d'];
export function TrendsPage() {
const navigate = useNavigate();
const [ticker, setTicker] = useState('');
const [window, setWindow] = useState<string | undefined>(undefined);
const { data, isLoading } = useTrends({ ticker: ticker || undefined, window, limit: 100 });
if (isLoading) return <LoadingSpinner />;
return (
<div>
<div className="mb-4 flex items-center justify-between">
<h1 className="text-xl font-semibold text-gray-100">Trends</h1>
<div className="flex items-center gap-3">
<TickerFilter value={ticker} onChange={setTicker} />
<div className="inline-flex rounded-md border border-surface-700" role="group" aria-label="Window selector">
<button
onClick={() => setWindow(undefined)}
className={`px-2 py-1 text-xs font-medium first:rounded-l-md last:rounded-r-md ${!window ? 'bg-brand-600 text-white' : 'bg-surface-900 text-gray-400 hover:bg-surface-800'}`}
>
All
</button>
{WINDOWS.map((w) => (
<button
key={w}
onClick={() => setWindow(w)}
className={`px-2 py-1 text-xs font-medium last:rounded-r-md ${window === w ? 'bg-brand-600 text-white' : 'bg-surface-900 text-gray-400 hover:bg-surface-800'}`}
>
{w}
</button>
))}
</div>
</div>
</div>
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{(data ?? []).map((trend) => (
<TrendCard key={trend.id} trend={trend} onClick={() => navigate({ to: '/trends/$id', params: { id: trend.id } })} />
))}
{data?.length === 0 && <p className="col-span-full text-center text-gray-500">No trends found</p>}
</div>
</div>
);
}
function TrendCard({ trend, onClick }: { trend: TrendSummary; onClick: () => void }) {
const [expanded, setExpanded] = useState(false);
return (
<Card className="cursor-pointer transition-colors hover:border-brand-500/50">
<div onClick={onClick}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="font-mono font-semibold text-brand-300">{trend.entity_id}</span>
<TrendArrow direction={trend.trend_direction} />
</div>
<span className="rounded bg-surface-800 px-2 py-0.5 text-xs text-gray-400">{trend.window}</span>
</div>
<div className="mt-3 space-y-2">
<div className="flex items-center justify-between text-xs">
<span className="text-gray-500">Strength</span>
<div className="h-1.5 w-24 rounded-full bg-surface-700">
<div className="h-full rounded-full bg-brand-500" style={{ width: `${trend.trend_strength * 100}%` }} />
</div>
</div>
<div className="flex items-center justify-between text-xs">
<span className="text-gray-500">Confidence</span>
<ConfidenceBar value={trend.confidence} />
</div>
<div className="flex items-center justify-between text-xs">
<span className="text-gray-500">Contradiction</span>
<span className={`font-mono ${trend.contradiction_score > 0.5 ? 'text-yellow-400' : 'text-gray-400'}`}>
{(trend.contradiction_score * 100).toFixed(0)}%
</span>
</div>
</div>
{trend.dominant_catalysts && trend.dominant_catalysts.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{trend.dominant_catalysts.map((c, i) => (
<span key={i} className="rounded bg-surface-800 px-1.5 py-0.5 text-[10px] text-gray-400">{c}</span>
))}
</div>
)}
</div>
{/* Expandable evidence preview */}
{(trend.top_supporting_evidence?.length || trend.top_opposing_evidence?.length) && (
<button
onClick={(e) => { e.stopPropagation(); setExpanded(!expanded); }}
className="mt-2 text-xs text-brand-400 hover:underline"
>
{expanded ? 'Hide' : 'Show'} evidence ({(trend.top_supporting_evidence?.length ?? 0) + (trend.top_opposing_evidence?.length ?? 0)})
</button>
)}
{expanded && (
<div className="mt-2 space-y-1 text-xs">
{trend.top_supporting_evidence?.map((e, i) => (
<div key={i} className="truncate text-green-400">+ {e}</div>
))}
{trend.top_opposing_evidence?.map((e, i) => (
<div key={i} className="truncate text-red-400"> {e}</div>
))}
</div>
)}
</Card>
);
}