← back to Patty
components/dashboard/DashboardTab.tsx
176 lines
'use client';
import { useState, useEffect } from 'react';
import {
Radio, Newspaper, BarChart3, Activity,
Megaphone, FileText, RefreshCw, Loader2,
DollarSign, TrendingUp,
} from 'lucide-react';
interface PulseStats {
total_feeds: number;
active_feeds: number;
total_articles: number;
articles_today: number;
avg_sentiment: number;
top_topics: string[];
last_fetch: string;
}
interface PetitionCounts {
draft: number;
active: number;
all: number;
}
function StatCard({ icon: Icon, label, value, sub, color }: {
icon: React.ElementType; label: string; value: string | number; sub?: string; color: string;
}) {
return (
<div style={{
padding: 20, borderRadius: 12,
backgroundColor: 'var(--color-surface)',
border: '1px solid var(--color-border)',
display: 'flex', alignItems: 'flex-start', gap: 14,
}}>
<div style={{
width: 40, height: 40, borderRadius: 10, backgroundColor: color + '18',
display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
}}>
<Icon size={20} style={{ color }} />
</div>
<div>
<div style={{ fontSize: 11, color: 'var(--color-text-muted)', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.04em' }}>{label}</div>
<div style={{ fontSize: 28, fontWeight: 700, color: 'var(--color-text)', lineHeight: 1.2 }}>{value}</div>
{sub && <div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginTop: 2 }}>{sub}</div>}
</div>
</div>
);
}
export default function DashboardTab() {
const [stats, setStats] = useState<PulseStats | null>(null);
const [petitions, setPetitions] = useState<PetitionCounts | null>(null);
const [grantStats, setGrantStats] = useState<{ grant_count: number; total_funding: number } | null>(null);
const [loading, setLoading] = useState(true);
const [fetching, setFetching] = useState(false);
async function loadData() {
setLoading(true);
try {
const [pulseRes, petRes, grantRes] = await Promise.all([
fetch('/api/pulse/stats'),
fetch('/api/petitions'),
fetch('/api/grants-proxy/dashboard').catch(() => null),
]);
if (pulseRes.ok) setStats(await pulseRes.json());
if (petRes.ok) {
const d = await petRes.json();
setPetitions(d.statusCounts || { draft: 0, active: 0, all: 0 });
}
if (grantRes?.ok) {
const g = await grantRes.json();
setGrantStats({ grant_count: g.grant_count ?? 0, total_funding: g.total_funding ?? 0 });
}
} catch (err) { console.error('Dashboard load error:', err); }
finally { setLoading(false); }
}
useEffect(() => { loadData(); }, []);
async function triggerFetch() {
setFetching(true);
try {
await fetch('/api/pulse/fetch', { method: 'POST' });
await loadData();
} catch {}
finally { setFetching(false); }
}
function sentimentLabel(s: number) {
if (s > 0.1) return { text: 'Positive', color: '#22c55e' };
if (s < -0.1) return { text: 'Negative', color: '#ef4444' };
return { text: 'Neutral', color: '#a78bfa' };
}
if (loading) {
return (
<div style={{ padding: 32, color: 'var(--color-text-muted)' }}>
<Loader2 size={20} style={{ animation: 'spin 1s linear infinite' }} /> Loading dashboard...
</div>
);
}
const sent = stats ? sentimentLabel(stats.avg_sentiment) : { text: '-', color: '#888' };
return (
<div style={{ padding: 24, maxWidth: 1200 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 20 }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: 'var(--color-text)', margin: 0 }}>Dashboard</h2>
{stats?.last_fetch && (
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginTop: 4 }}>
Last fetch: {new Date(stats.last_fetch).toLocaleString('en-US', { timeZone: 'America/Los_Angeles', hour: 'numeric', minute: '2-digit', hour12: true })} PT
</div>
)}
</div>
<button
onClick={triggerFetch}
disabled={fetching}
style={{
display: 'flex', alignItems: 'center', gap: 6, padding: '8px 16px',
borderRadius: 8, border: '1px solid var(--color-border)',
backgroundColor: 'var(--color-surface)', color: 'var(--color-text)',
cursor: fetching ? 'wait' : 'pointer', fontSize: 13, fontWeight: 500,
}}
>
{fetching ? <Loader2 size={14} style={{ animation: 'spin 1s linear infinite' }} /> : <RefreshCw size={14} />}
{fetching ? 'Fetching...' : 'Fetch News'}
</button>
</div>
{/* Stat Cards */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(220px, 1fr))', gap: 16, marginBottom: 28 }}>
<StatCard icon={Radio} label="Active Feeds" value={stats?.active_feeds ?? '-'} sub={`${stats?.total_feeds ?? 0} total`} color="#3b82f6" />
<StatCard icon={Newspaper} label="Articles Today" value={stats?.articles_today?.toLocaleString() ?? '-'} sub={`${stats?.total_articles?.toLocaleString() ?? 0} total`} color="#8b5cf6" />
<StatCard icon={Activity} label="Avg Sentiment" value={sent.text} sub={stats ? stats.avg_sentiment.toFixed(3) : '-'} color={sent.color} />
<StatCard icon={Megaphone} label="Active Petitions" value={petitions?.active ?? '-'} sub={`${petitions?.all ?? 0} total`} color="#f59e0b" />
<StatCard icon={FileText} label="Draft Petitions" value={petitions?.draft ?? '-'} color="#6b7280" />
<StatCard icon={BarChart3} label="Topics Generated" value={stats?.top_topics?.length ?? '-'} color="#10b981" />
<StatCard icon={DollarSign} label="Grants Found" value={grantStats?.grant_count ?? '-'} color="#059669" />
<StatCard icon={TrendingUp} label="Total Funding" value={grantStats?.total_funding ? `$${grantStats.total_funding.toLocaleString()}` : '$0'} color="#059669" />
</div>
{/* Top Topics */}
{stats?.top_topics && stats.top_topics.length > 0 && (
<div style={{
padding: 20, borderRadius: 12,
backgroundColor: 'var(--color-surface)',
border: '1px solid var(--color-border)',
}}>
<h3 style={{ fontSize: 14, fontWeight: 600, color: 'var(--color-text)', marginBottom: 12 }}>
Top AI Topics
</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{stats.top_topics.map((t, i) => (
<div key={i} style={{
display: 'flex', alignItems: 'center', gap: 10,
padding: '8px 12px', borderRadius: 8,
backgroundColor: 'var(--color-bg)',
}}>
<span style={{
width: 22, height: 22, borderRadius: 6,
backgroundColor: '#7c3aed22', color: '#a78bfa',
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 11, fontWeight: 700, flexShrink: 0,
}}>{i + 1}</span>
<span style={{ fontSize: 13, color: 'var(--color-text)' }}>{t}</span>
</div>
))}
</div>
</div>
)}
</div>
);
}