feat: competitive intelligence & historical pattern matching layer
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
import { useGlobalEvents } from '../api/hooks';
|
||||
import { DataTable, type Column } from '../components/DataTable';
|
||||
import { StatusBadge, LoadingSpinner } from '../components/ui';
|
||||
import type { GlobalEvent } from '../api/hooks';
|
||||
|
||||
const SEVERITIES = ['low', 'moderate', 'high', 'critical'];
|
||||
|
||||
const severityColors: Record<string, string> = {
|
||||
critical: 'bg-red-900/40 text-red-400 border-red-700/50',
|
||||
high: 'bg-orange-900/40 text-orange-400 border-orange-700/50',
|
||||
moderate: 'bg-yellow-900/40 text-yellow-400 border-yellow-700/50',
|
||||
low: 'bg-green-900/40 text-green-400 border-green-700/50',
|
||||
};
|
||||
|
||||
function SeverityBadge({ severity }: { severity: string }) {
|
||||
const cls = severityColors[severity] ?? 'bg-gray-800/40 text-gray-400 border-gray-700/50';
|
||||
return (
|
||||
<span className={`inline-block rounded-full border px-2 py-0.5 text-xs font-medium ${cls}`}>
|
||||
{severity}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const columns: Column<GlobalEvent>[] = [
|
||||
{
|
||||
key: 'summary',
|
||||
header: 'Summary',
|
||||
render: (r) => <span className="line-clamp-1 max-w-sm">{r.summary}</span>,
|
||||
},
|
||||
{
|
||||
key: 'event_types',
|
||||
header: 'Impact Types',
|
||||
render: (r) => (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{r.event_types.map((t) => (
|
||||
<span key={t} className="rounded bg-surface-800 px-1.5 py-0.5 text-[10px] text-gray-400">{t.replace(/_/g, ' ')}</span>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'severity',
|
||||
header: 'Severity',
|
||||
render: (r) => <SeverityBadge severity={r.severity} />,
|
||||
},
|
||||
{
|
||||
key: 'affected_regions',
|
||||
header: 'Regions',
|
||||
render: (r) => (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{r.affected_regions.slice(0, 4).map((reg) => (
|
||||
<span key={reg} className="rounded bg-surface-800 px-1.5 py-0.5 text-[10px] text-gray-400">{reg}</span>
|
||||
))}
|
||||
{r.affected_regions.length > 4 && <span className="text-[10px] text-gray-500">+{r.affected_regions.length - 4}</span>}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'affected_sectors',
|
||||
header: 'Sectors',
|
||||
render: (r) => (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{r.affected_sectors.slice(0, 3).map((s) => (
|
||||
<span key={s} className="rounded bg-surface-800 px-1.5 py-0.5 text-[10px] text-gray-400">{s}</span>
|
||||
))}
|
||||
{r.affected_sectors.length > 3 && <span className="text-[10px] text-gray-500">+{r.affected_sectors.length - 3}</span>}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'created_at',
|
||||
header: 'Event Date',
|
||||
render: (r) => <span className="text-xs">{new Date(r.created_at).toLocaleDateString()}</span>,
|
||||
},
|
||||
];
|
||||
|
||||
export function GlobalEventsPage() {
|
||||
const navigate = useNavigate();
|
||||
const [severity, setSeverity] = useState('');
|
||||
const { data, isLoading, error } = useGlobalEvents({
|
||||
severity: severity || undefined,
|
||||
limit: 100,
|
||||
});
|
||||
|
||||
if (isLoading) return <LoadingSpinner />;
|
||||
if (error) return <div className="text-red-400">Failed to load global events</div>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h1 className="text-xl font-semibold text-gray-100">Global Events</h1>
|
||||
<div className="inline-flex rounded-md border border-surface-700" role="group" aria-label="Severity filter">
|
||||
<button
|
||||
onClick={() => setSeverity('')}
|
||||
className={`px-2 py-1 text-xs font-medium first:rounded-l-md last:rounded-r-md ${!severity ? 'bg-brand-600 text-white' : 'bg-surface-900 text-gray-400 hover:bg-surface-800'}`}
|
||||
>
|
||||
All
|
||||
</button>
|
||||
{SEVERITIES.map((s) => (
|
||||
<button
|
||||
key={s}
|
||||
onClick={() => setSeverity(s)}
|
||||
className={`px-2 py-1 text-xs font-medium capitalize last:rounded-r-md ${severity === s ? 'bg-brand-600 text-white' : 'bg-surface-900 text-gray-400 hover:bg-surface-800'}`}
|
||||
>
|
||||
{s}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<DataTable<GlobalEvent>
|
||||
data={data ?? []}
|
||||
columns={columns}
|
||||
keyField="id"
|
||||
onRowClick={(row) => navigate({ to: '/macro/events/$id', params: { id: row.id } })}
|
||||
filterFn={(row, q) => {
|
||||
const lq = q.toLowerCase();
|
||||
return row.summary.toLowerCase().includes(lq) || row.event_types.some((t) => t.toLowerCase().includes(lq));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user