← back to Grant
components/dashboard/DashboardTab.tsx
448 lines
'use client';
import { useState, useEffect } from 'react';
import { SkeletonStats, SkeletonList } from '../Skeleton';
import {
Award,
Newspaper,
Users,
Megaphone,
TrendingUp,
DollarSign,
Calendar,
Target,
FileText,
Clock,
} from 'lucide-react';
/* ─── Types ──────────────────────────────────────────────────────────────── */
interface DashboardData {
org_name: string;
grant_count: number;
total_funding: number;
news_count: number;
collab_count: number;
proposal_count: number;
upcoming_deadlines: {
id: string;
title: string;
funder: string;
deadline: string;
status: string;
priority: string;
ai_fit_score: number | null;
}[];
recent_activity: {
id: string;
type: 'grant' | 'collaboration' | 'proposal';
label: string;
detail: string;
status: string;
created_at: string;
}[];
}
function formatCurrency(n: number): string {
if (n === 0) return '$0';
return '$' + n.toLocaleString('en-US', { maximumFractionDigits: 0 });
}
function formatDate(d: string): string {
return new Date(d).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
});
}
function timeAgo(d: string): string {
const diff = Date.now() - new Date(d).getTime();
const mins = Math.floor(diff / 60000);
if (mins < 1) return 'just now';
if (mins < 60) return `${mins}m ago`;
const hours = Math.floor(mins / 60);
if (hours < 24) return `${hours}h ago`;
const days = Math.floor(hours / 24);
return `${days}d ago`;
}
function daysUntil(d: string): number {
return Math.ceil((new Date(d).getTime() - Date.now()) / (1000 * 60 * 60 * 24));
}
const ACTIVITY_ICONS: Record<string, React.ElementType> = {
grant: Award,
collaboration: Users,
proposal: FileText,
};
const ACTIVITY_COLORS: Record<string, string> = {
grant: '#059669',
collaboration: '#8b5cf6',
proposal: '#3b82f6',
};
/* ─── Stat Card ──────────────────────────────────────────────────────────── */
function StatCard({
label,
value,
Icon,
accent,
}: {
label: string;
value: string;
Icon: React.ElementType;
accent: string;
}) {
return (
<div className="stat-card">
<div className="flex items-center justify-between mb-3">
<div
className="w-9 h-9 rounded-lg flex items-center justify-center"
style={{
backgroundColor: `${accent}18`,
border: `1px solid ${accent}30`,
}}
>
<Icon size={18} style={{ color: accent }} />
</div>
</div>
<div className="text-2xl font-bold mb-1" style={{ color: 'var(--color-text)' }}>
{value}
</div>
<div className="text-xs" style={{ color: 'var(--color-text-muted)' }}>
{label}
</div>
</div>
);
}
/* ─── Dashboard Tab ──────────────────────────────────────────────────────── */
interface DashboardTabProps {
onNavigate?: (tab: string) => void;
}
export default function DashboardTab({ onNavigate }: DashboardTabProps) {
const [data, setData] = useState<DashboardData | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
(async () => {
try {
const res = await fetch('/api/dashboard', { credentials: 'include' });
if (res.ok) {
setData(await res.json());
}
} catch {
// fetch error handled by loading state
} finally {
setLoading(false);
}
})();
}, []);
if (loading) {
return (
<div className="p-6" style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
<SkeletonStats count={4} />
<SkeletonList count={3} />
</div>
);
}
if (!data) {
return (
<div className="p-6">
<div className="card text-center py-16">
<p style={{ color: 'var(--color-text-muted)' }}>Failed to load dashboard data.</p>
</div>
</div>
);
}
return (
<div className="p-6 space-y-6">
{/* Welcome Banner */}
<div
className="rounded-xl p-6"
style={{
background: 'linear-gradient(135deg, rgba(5,150,105,0.15), rgba(4,120,87,0.08))',
border: '1px solid rgba(5,150,105,0.2)',
}}
>
<h2 className="text-xl font-bold mb-2" style={{ color: 'var(--color-text)' }}>
{data.org_name}
</h2>
<p
className="text-sm leading-relaxed max-w-2xl"
style={{ color: 'var(--color-text-secondary)' }}
>
Your AI-powered fundraising command center. Discover grants, monitor relevant news,
build collaborations, and manage outreach -- all in one place.
</p>
</div>
{/* Stats Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<StatCard
label="Active Grants"
value={String(data.grant_count)}
Icon={Award}
accent="#059669"
/>
<StatCard
label="Total Funding"
value={formatCurrency(data.total_funding)}
Icon={DollarSign}
accent="#f59e0b"
/>
<StatCard
label="News Items"
value={String(data.news_count)}
Icon={Newspaper}
accent="#3b82f6"
/>
<StatCard
label="Collaborations"
value={String(data.collab_count)}
Icon={Users}
accent="#8b5cf6"
/>
</div>
{/* Two-Column Layout */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Upcoming Deadlines */}
<div className="card">
<div className="flex items-center gap-2 mb-4">
<Calendar size={16} style={{ color: 'var(--color-primary)' }} />
<h3 className="text-sm font-semibold" style={{ color: 'var(--color-text)' }}>
Upcoming Deadlines
</h3>
</div>
{data.upcoming_deadlines.length === 0 ? (
<div className="flex flex-col items-center justify-center py-8 text-center">
<div
className="w-10 h-10 rounded-lg flex items-center justify-center mb-3"
style={{
backgroundColor: 'var(--color-surface-el)',
border: '1px solid var(--color-border)',
}}
>
<Calendar size={18} style={{ color: 'var(--color-text-muted)' }} />
</div>
<p className="text-sm" style={{ color: 'var(--color-text-muted)' }}>
No upcoming deadlines
</p>
<p className="text-xs mt-1" style={{ color: 'var(--color-text-muted)' }}>
Discovered grants with deadlines will appear here
</p>
</div>
) : (
<div className="space-y-2">
{data.upcoming_deadlines.map((g) => {
const days = daysUntil(g.deadline);
const isUrgent = days <= 14;
return (
<div
key={g.id}
className="flex items-center justify-between p-3 rounded-lg"
style={{
backgroundColor: 'var(--color-surface-el)',
border: `1px solid ${isUrgent ? 'rgba(239,68,68,0.3)' : 'var(--color-border)'}`,
}}
>
<div className="min-w-0 flex-1">
<p className="text-sm font-medium truncate" style={{ color: 'var(--color-text)' }}>
{g.title}
</p>
<p className="text-xs" style={{ color: 'var(--color-text-muted)' }}>
{g.funder}
</p>
</div>
<div className="flex items-center gap-2 shrink-0 ml-3">
<div className="text-right">
<p
className="text-xs font-semibold"
style={{ color: isUrgent ? '#ef4444' : 'var(--color-text-secondary)' }}
>
{formatDate(g.deadline)}
</p>
<p
className="text-xs"
style={{ color: isUrgent ? '#ef4444' : 'var(--color-text-muted)' }}
>
{days}d left
</p>
</div>
{isUrgent && (
<Clock size={14} style={{ color: '#ef4444' }} />
)}
</div>
</div>
);
})}
</div>
)}
</div>
{/* Recent Activity */}
<div className="card">
<div className="flex items-center gap-2 mb-4">
<TrendingUp size={16} style={{ color: 'var(--color-primary)' }} />
<h3 className="text-sm font-semibold" style={{ color: 'var(--color-text)' }}>
Recent Activity
</h3>
</div>
{data.recent_activity.length === 0 ? (
<div className="flex flex-col items-center justify-center py-8 text-center">
<div
className="w-10 h-10 rounded-lg flex items-center justify-center mb-3"
style={{
backgroundColor: 'var(--color-surface-el)',
border: '1px solid var(--color-border)',
}}
>
<TrendingUp size={18} style={{ color: 'var(--color-text-muted)' }} />
</div>
<p className="text-sm" style={{ color: 'var(--color-text-muted)' }}>
No recent activity
</p>
<p className="text-xs mt-1" style={{ color: 'var(--color-text-muted)' }}>
Your actions and AI-generated insights will appear here
</p>
</div>
) : (
<div className="space-y-2">
{data.recent_activity.map((a) => {
const ActivityIcon = ACTIVITY_ICONS[a.type] || Award;
const activityColor = ACTIVITY_COLORS[a.type] || '#059669';
return (
<div
key={a.id}
className="flex items-start gap-3 p-3 rounded-lg"
style={{
backgroundColor: 'var(--color-surface-el)',
border: '1px solid var(--color-border)',
}}
>
<div
className="w-7 h-7 rounded-md flex items-center justify-center shrink-0 mt-0.5"
style={{
backgroundColor: `${activityColor}18`,
border: `1px solid ${activityColor}30`,
}}
>
<ActivityIcon size={13} style={{ color: activityColor }} />
</div>
<div className="flex-1 min-w-0">
<p className="text-xs font-medium truncate" style={{ color: 'var(--color-text)' }}>
{a.label}
</p>
<p className="text-xs truncate" style={{ color: 'var(--color-text-muted)' }}>
{a.detail}
</p>
</div>
<span className="text-xs shrink-0" style={{ color: 'var(--color-text-muted)' }}>
{timeAgo(a.created_at)}
</span>
</div>
);
})}
</div>
)}
</div>
</div>
{/* Quick Actions */}
<div className="card">
<div className="flex items-center gap-2 mb-4">
<Target size={16} style={{ color: 'var(--color-primary)' }} />
<h3 className="text-sm font-semibold" style={{ color: 'var(--color-text)' }}>
Quick Actions
</h3>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
<QuickAction
Icon={Award}
label="Discover Grants"
description="Find new funding opportunities"
onClick={() => onNavigate?.('grants')}
/>
<QuickAction
Icon={Newspaper}
label="Scan News"
description="Monitor relevant headlines"
onClick={() => onNavigate?.('news')}
/>
<QuickAction
Icon={Users}
label="Find Partners"
description="AI-suggested collaborations"
onClick={() => onNavigate?.('collaborations')}
/>
<QuickAction
Icon={Megaphone}
label="Draft Outreach"
description="Generate outreach emails"
onClick={() => onNavigate?.('outreach')}
/>
</div>
</div>
</div>
);
}
function QuickAction({
Icon,
label,
description,
onClick,
}: {
Icon: React.ElementType;
label: string;
description: string;
onClick?: () => void;
}) {
return (
<button
className="flex items-start gap-3 p-3 rounded-lg text-left w-full"
onClick={onClick}
style={{
backgroundColor: 'var(--color-surface-el)',
border: '1px solid var(--color-border)',
transition: 'border-color 0.15s, background-color 0.15s',
cursor: 'pointer',
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = 'rgba(5,150,105,0.4)';
e.currentTarget.style.backgroundColor = 'rgba(5,150,105,0.06)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = 'var(--color-border)';
e.currentTarget.style.backgroundColor = 'var(--color-surface-el)';
}}
>
<div
className="w-8 h-8 rounded-md flex items-center justify-center shrink-0 mt-0.5"
style={{
backgroundColor: 'rgba(5,150,105,0.12)',
border: '1px solid rgba(5,150,105,0.2)',
}}
>
<Icon size={15} style={{ color: '#34d399' }} />
</div>
<div>
<div className="text-sm font-medium" style={{ color: 'var(--color-text)' }}>
{label}
</div>
<div className="text-xs mt-0.5" style={{ color: 'var(--color-text-muted)' }}>
{description}
</div>
</div>
</button>
);
}