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