← back to Norma
components/dashboard/DashboardTab.tsx
1090 lines
'use client';
/**
* DashboardTab — Priority landing page.
*
* Usage:
* <DashboardTab onNavigate={(tab) => setActiveTab(tab)} />
*
* Sections:
* 1. Priority Banner — most recent urgent alert
* 2. Stats Row — 4 KPI cards (drafts, petitions, grants, media)
* 3. Two-column body — Action Items (left 60%) + Recent Activity (right 40%)
* 4. Quick Actions — shortcut buttons to primary tabs
*/
import { useState, useEffect, useCallback } from 'react';
import {
AlertTriangle,
FileText,
Megaphone,
Award,
Newspaper,
ArrowRight,
CheckCircle2,
Clock,
Calendar,
Zap,
Plus,
Search,
GitBranch,
Activity,
RefreshCw,
Bell,
TrendingUp,
DollarSign,
Users,
Vote,
Building2,
Mail,
} from 'lucide-react';
import IntelDashboardWidget from '@/components/intelligence/IntelDashboardWidget';
import MemberEmailModal from '@/components/email-sends/MemberEmailModal';
/* ─── Types ─────────────────────────────────────────────────────────────── */
interface AlertItem {
id: string;
type: 'news' | 'grant' | 'petition' | 'congress' | 'donation' | 'community' | 'organization';
title: string;
summary: string;
date: string;
source: string;
}
interface Session {
id: string;
title?: string;
session_date: string;
status: string;
created_at: string;
drafts?: Array<{ id: string; lane: string; status: string }>;
}
interface Petition {
id: string;
title: string;
status: string;
signature_count?: number;
signature_goal?: number;
ai_urgency?: number;
deadline?: string;
}
interface Grant {
id: string;
title: string;
funder?: string;
deadline?: string;
amount_max?: number;
status: string;
priority?: string;
}
interface ActionItem {
id: string;
priority: 'red' | 'amber' | 'green';
type: 'draft' | 'petition' | 'grant' | 'media';
title: string;
subtitle?: string;
actionLabel: string;
targetTab: string;
icon: typeof FileText;
}
interface DashboardData {
urgentAlert: AlertItem | null;
pendingDraftsCount: number;
activePetitionsCount: number;
grantDeadlinesCount: number;
mediaAlertsCount: number;
recentSessions: Session[];
activePetitions: Petition[];
upcomingGrants: Grant[];
recentActivity: AlertItem[];
}
export interface DashboardTabProps {
onNavigate?: (tab: string) => void;
}
/* ─── Constants ─────────────────────────────────────────────────────────── */
// NOTE: Hex colors required for template-literal rgba tints (e.g. `${color}18`).
// CSS variable references cannot be used inside rgba() template strings.
const ALERT_TYPE_META: Record<
AlertItem['type'],
{ icon: typeof Newspaper; color: string; label: string }
> = {
news: { icon: Newspaper, color: '#06b6d4', label: 'News' },
grant: { icon: Award, color: '#f59e0b', label: 'Grant' },
petition: { icon: Megaphone, color: '#8b5cf6', label: 'Petition' },
congress: { icon: Vote, color: '#6366f1', label: 'Congress' },
donation: { icon: DollarSign, color: '#22c55e', label: 'Donation' },
community: { icon: Users, color: '#f97316', label: 'Community' },
organization: { icon: Building2, color: '#34d399', label: 'Org' },
};
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
/* ─── Helpers ────────────────────────────────────────────────────────────── */
function relativeTime(iso: string): string {
if (!iso) return '';
const diffMs = Date.now() - new Date(iso).getTime();
if (isNaN(diffMs) || diffMs < 0) return 'just now';
const minutes = Math.floor(diffMs / 60_000);
if (minutes < 1) return 'just now';
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
const days = Math.floor(hours / 24);
if (days === 1) return 'Yesterday';
if (days < 7) return `${days}d ago`;
const weeks = Math.floor(days / 7);
if (weeks < 5) return `${weeks}w ago`;
return `${Math.floor(days / 30)}mo ago`;
}
function daysUntil(iso: string): number {
if (!iso) return Infinity;
return Math.ceil((new Date(iso).getTime() - Date.now()) / 86_400_000);
}
function groupByRecency(items: AlertItem[]): Record<string, AlertItem[]> {
const now = Date.now();
const groups: Record<string, AlertItem[]> = {
Today: [],
Yesterday: [],
'This Week': [],
};
for (const item of items) {
const diffH = (now - new Date(item.date).getTime()) / 3_600_000;
if (diffH < 24) groups['Today'].push(item);
else if (diffH < 48) groups['Yesterday'].push(item);
else groups['This Week'].push(item);
}
return groups;
}
function buildActionItems(
sessions: Session[],
petitions: Petition[],
grants: Grant[]
): ActionItem[] {
const items: ActionItem[] = [];
// Pending email drafts — highest urgency first (most recent unsent)
const pendingSessions = sessions
.filter((s) => s.status !== 'sent')
.slice(0, 3);
for (const s of pendingSessions) {
items.push({
id: `draft-${s.id}`,
priority: s.status === 'review' ? 'red' : 'amber',
type: 'draft',
title: s.title ?? `Batch ${new Date(s.session_date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}`,
subtitle: `Status: ${s.status}`,
actionLabel: 'Review',
targetTab: 'drafts',
icon: FileText,
});
}
// Petitions approaching signature milestones
const urgentPetitions = petitions
.filter((p) => {
if (!p.signature_count || !p.signature_goal) return false;
const ratio = p.signature_count / p.signature_goal;
return ratio >= 0.8 && ratio < 1;
})
.slice(0, 2);
for (const p of urgentPetitions) {
const pct = p.signature_count && p.signature_goal
? Math.round((p.signature_count / p.signature_goal) * 100)
: 0;
items.push({
id: `petition-${p.id}`,
priority: pct >= 90 ? 'red' : 'amber',
type: 'petition',
title: p.title,
subtitle: `${pct}% of goal reached`,
actionLabel: 'View',
targetTab: 'petitions',
icon: Megaphone,
});
}
// Grants with upcoming deadlines (within 30 days)
const urgentGrants = grants
.filter((g) => {
if (!g.deadline) return false;
const d = daysUntil(g.deadline);
return d >= 0 && d <= 30;
})
.sort((a, b) => daysUntil(a.deadline!) - daysUntil(b.deadline!))
.slice(0, 3);
for (const g of urgentGrants) {
const d = daysUntil(g.deadline!);
items.push({
id: `grant-${g.id}`,
priority: d <= 7 ? 'red' : d <= 14 ? 'amber' : 'green',
type: 'grant',
title: g.title,
subtitle: `Due in ${d}d${g.funder ? ` · ${g.funder}` : ''}`,
actionLabel: 'Apply',
targetTab: 'agent-grants',
icon: Award,
});
}
// Sort: red → amber → green, max 8
const ORDER = { red: 0, amber: 1, green: 2 } as const;
return items
.sort((a, b) => ORDER[a.priority] - ORDER[b.priority])
.slice(0, 8);
}
/* ─── Sub-components ────────────────────────────────────────────────────── */
function PriorityDot({ level }: { level: 'red' | 'amber' | 'green' }) {
const colorMap = { red: '#f43f5e', amber: '#f59e0b', green: '#10b981' };
return (
<span
aria-hidden="true"
style={{
display: 'inline-block',
width: 8,
height: 8,
borderRadius: '50%',
backgroundColor: colorMap[level],
flexShrink: 0,
}}
/>
);
}
function StatCard({
icon: Icon,
count,
label,
accentColor,
loading,
onClick,
}: {
icon: typeof FileText;
count: number;
label: string;
accentColor: string;
loading: boolean;
onClick?: () => void;
}) {
return (
<button
type="button"
onClick={onClick}
className="card card-enter hover-lift"
style={{
flex: '1 1 0',
minWidth: 120,
display: 'flex',
flexDirection: 'column',
gap: 8,
padding: '1rem',
cursor: onClick ? 'pointer' : 'default',
textAlign: 'left',
border: '1px solid var(--color-border)',
background: `linear-gradient(135deg, var(--color-surface) 0%, ${accentColor}08 100%)`,
}}
>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<div
style={{
width: 32,
height: 32,
borderRadius: 8,
backgroundColor: `${accentColor}18`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Icon size={16} style={{ color: accentColor }} aria-hidden="true" />
</div>
{loading ? (
<div className="skeleton" style={{ width: 36, height: 28, borderRadius: 6 }} />
) : (
<span
className="num"
style={{
fontSize: '1.75rem',
fontWeight: 700,
lineHeight: 1,
color: 'var(--color-text)',
}}
>
{count}
</span>
)}
</div>
<span className="stat-label">{label}</span>
</button>
);
}
function SkeletonRows({ count }: { count: number }) {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{Array.from({ length: count }).map((_, i) => (
<div key={i} className="skeleton" style={{ height: 44, borderRadius: 8 }} />
))}
</div>
);
}
/* ─── Main Component ─────────────────────────────────────────────────────── */
export default function DashboardTab({ onNavigate }: DashboardTabProps) {
const [data, setData] = useState<DashboardData>({
urgentAlert: null,
pendingDraftsCount: 0,
activePetitionsCount: 0,
grantDeadlinesCount: 0,
mediaAlertsCount: 0,
recentSessions: [],
activePetitions: [],
upcomingGrants: [],
recentActivity: [],
});
const [loading, setLoading] = useState(true);
const [sectionsLoading, setSectionsLoading] = useState({
banner: true,
stats: true,
actions: true,
activity: true,
});
const [memberEmailOpen, setMemberEmailOpen] = useState(false);
const fetchAll = useCallback(async () => {
setLoading(true);
setSectionsLoading({ banner: true, stats: true, actions: true, activity: true });
// Fire all requests in parallel — failures in individual endpoints are isolated
const [
urgentAlertRes,
sessionsRes,
petitionsRes,
grantsRes,
activityRes,
mediaCountRes,
] = await Promise.allSettled([
fetch('/api/alerts?categories=news_media,grants,petitions&limit=1'),
fetch('/api/sessions'),
fetch('/api/petitions?status=active'),
fetch('/api/grants?upcoming=true'),
fetch('/api/alerts?limit=10'),
fetch('/api/alerts?categories=news_media&limit=1'),
]);
// ── Banner: most urgent alert
let urgentAlert: AlertItem | null = null;
if (urgentAlertRes.status === 'fulfilled' && urgentAlertRes.value.ok) {
try {
const d = await urgentAlertRes.value.json();
urgentAlert = d.alerts?.[0] ?? null;
} catch { /* ignore */ }
}
setSectionsLoading((p) => ({ ...p, banner: false }));
// ── Sessions: pending drafts count + action items
let sessions: Session[] = [];
let pendingDraftsCount = 0;
if (sessionsRes.status === 'fulfilled' && sessionsRes.value.ok) {
try {
const d = await sessionsRes.value.json();
sessions = d.sessions ?? d ?? [];
pendingDraftsCount = sessions.filter((s) => s.status !== 'sent').length;
} catch { /* ignore */ }
}
// ── Petitions: active count + action items
let petitions: Petition[] = [];
let activePetitionsCount = 0;
if (petitionsRes.status === 'fulfilled' && petitionsRes.value.ok) {
try {
const d = await petitionsRes.value.json();
petitions = d.petitions ?? d ?? [];
activePetitionsCount = petitions.length;
} catch { /* ignore */ }
}
// ── Grants: deadline count + action items
let grants: Grant[] = [];
let grantDeadlinesCount = 0;
if (grantsRes.status === 'fulfilled' && grantsRes.value.ok) {
try {
const d = await grantsRes.value.json();
grants = d.grants ?? d ?? [];
grantDeadlinesCount = grants.filter((g) => {
if (!g.deadline) return false;
const ms = new Date(g.deadline).getTime() - Date.now();
return ms >= 0 && ms <= THIRTY_DAYS_MS;
}).length;
} catch { /* ignore */ }
}
setSectionsLoading((p) => ({ ...p, stats: false, actions: false }));
// ── Recent activity
let recentActivity: AlertItem[] = [];
if (activityRes.status === 'fulfilled' && activityRes.value.ok) {
try {
const d = await activityRes.value.json();
recentActivity = d.alerts ?? [];
} catch { /* ignore */ }
}
setSectionsLoading((p) => ({ ...p, activity: false }));
// ── Media alerts count (header of most recent only, count inferred from limit)
let mediaAlertsCount = 0;
if (mediaCountRes.status === 'fulfilled' && mediaCountRes.value.ok) {
try {
const d = await mediaCountRes.value.json();
mediaAlertsCount = (d.alerts ?? []).length;
} catch { /* ignore */ }
}
setData({
urgentAlert,
pendingDraftsCount,
activePetitionsCount,
grantDeadlinesCount,
mediaAlertsCount,
recentSessions: sessions.slice(0, 10),
activePetitions: petitions,
upcomingGrants: grants,
recentActivity,
});
setLoading(false);
}, []);
useEffect(() => {
fetchAll();
}, [fetchAll]);
const actionItems = buildActionItems(
data.recentSessions,
data.activePetitions,
data.upcomingGrants
);
const activityGroups = groupByRecency(data.recentActivity);
/* Determine banner severity */
const bannerColor =
data.urgentAlert?.type === 'grant'
? '#f43f5e' // red — grant deadlines are critical
: '#f59e0b'; // orange — general urgent
/* ── Render ─────────────────────────────────────────────────────────────── */
return (
<div
style={{
padding: '1.25rem 1.5rem',
maxWidth: 1200,
margin: '0 auto',
display: 'flex',
flexDirection: 'column',
gap: '1.25rem',
}}
>
{/* ── Toolbar ──────────────────────────────────────────────────────── */}
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: 12,
}}
>
<div>
<h1
style={{
fontFamily: 'var(--font-heading)',
fontSize: '1.25rem',
fontWeight: 700,
color: 'var(--color-text)',
margin: 0,
}}
>
Priority Dashboard
</h1>
<p style={{ fontSize: '0.8125rem', color: 'var(--color-text-muted)', margin: 0, marginTop: 2 }}>
At-a-glance view of what needs attention now
</p>
</div>
<button
type="button"
onClick={fetchAll}
disabled={loading}
className="btn btn-ghost btn-sm"
aria-label="Refresh dashboard"
title="Refresh all"
>
<RefreshCw
size={14}
aria-hidden="true"
style={{ animation: loading ? 'spin 0.7s linear infinite' : 'none' }}
/>
Refresh
</button>
</div>
{/* ── 1. Priority Banner ────────────────────────────────────────────── */}
{sectionsLoading.banner ? (
<div className="skeleton" style={{ height: 60, borderRadius: 10 }} />
) : data.urgentAlert ? (
<div
className="card breathe-glow-urgent"
role="alert"
aria-live="polite"
style={{
borderLeft: `4px solid ${bannerColor}`,
borderRadius: 'var(--radius-md)',
padding: '0.75rem 1rem',
display: 'flex',
alignItems: 'center',
gap: 12,
flexWrap: 'wrap',
}}
>
{/* Icon */}
<div
style={{
width: 32,
height: 32,
borderRadius: 8,
backgroundColor: `${bannerColor}18`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
}}
>
<AlertTriangle size={16} style={{ color: bannerColor }} aria-hidden="true" />
</div>
{/* Text */}
<div style={{ flex: 1, minWidth: 0 }}>
<span
className="badge badge-warning"
style={{ fontSize: '0.6rem', marginBottom: 3, display: 'inline-flex' }}
>
{ALERT_TYPE_META[data.urgentAlert.type]?.label ?? 'Alert'}
</span>
<p
style={{
fontSize: '0.875rem',
fontWeight: 500,
color: 'var(--color-text)',
margin: 0,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
title={data.urgentAlert.title}
>
{data.urgentAlert.title}
</p>
</div>
{/* Time */}
<span
style={{
fontSize: '0.7rem',
color: 'var(--color-text-muted)',
flexShrink: 0,
}}
>
<Clock size={11} style={{ display: 'inline', verticalAlign: 'middle', marginRight: 3 }} aria-hidden="true" />
{relativeTime(data.urgentAlert.date)}
</span>
{/* CTA */}
<button
type="button"
onClick={() => onNavigate?.('news')}
className="btn btn-sm"
style={{
borderColor: bannerColor,
color: bannerColor,
backgroundColor: `${bannerColor}10`,
flexShrink: 0,
}}
>
View
<ArrowRight size={13} aria-hidden="true" />
</button>
</div>
) : null}
{/* ── 2. Stats Row ──────────────────────────────────────────────────── */}
<div
style={{
display: 'flex',
gap: '0.875rem',
flexWrap: 'wrap',
}}
role="region"
aria-label="Key metrics"
>
<StatCard
icon={FileText}
count={data.pendingDraftsCount}
label="Pending Drafts"
accentColor="#6366f1"
loading={sectionsLoading.stats}
onClick={() => onNavigate?.('drafts')}
/>
<StatCard
icon={Megaphone}
count={data.activePetitionsCount}
label="Active Petitions"
accentColor="#f43f5e"
loading={sectionsLoading.stats}
onClick={() => onNavigate?.('petitions')}
/>
<StatCard
icon={Calendar}
count={data.grantDeadlinesCount}
label="Grant Deadlines (30d)"
accentColor="#f59e0b"
loading={sectionsLoading.stats}
onClick={() => onNavigate?.('agent-grants')}
/>
<StatCard
icon={Newspaper}
count={data.mediaAlertsCount}
label="Media Alerts"
accentColor="#06b6d4"
loading={sectionsLoading.stats}
onClick={() => onNavigate?.('news')}
/>
</div>
{/* ── 2b. Intelligence Widget ───────────────────────────────────────── */}
<IntelDashboardWidget onNavigate={onNavigate} />
{/* ── 3. Two-column Body ───────────────────────────────────────────── */}
<div
style={{
display: 'grid',
gridTemplateColumns: '3fr 2fr',
gap: '1rem',
}}
>
{/* Left: Action Items */}
<section
className="card card-enter card-enter-1"
style={{ padding: 0, overflow: 'hidden' }}
aria-labelledby="action-items-heading"
>
{/* Header */}
<div
className="section-header"
style={{ padding: '0.875rem 1rem', marginBottom: 0 }}
>
<Zap size={16} style={{ color: 'var(--color-secondary)' }} aria-hidden="true" />
<h2 id="action-items-heading" className="section-title" style={{ fontSize: '0.9375rem' }}>
Action Items
</h2>
{!sectionsLoading.actions && (
<span className="section-count">{actionItems.length}</span>
)}
</div>
{/* Body */}
<div style={{ padding: '0.5rem 0.875rem 1rem' }}>
{sectionsLoading.actions ? (
<SkeletonRows count={5} />
) : actionItems.length === 0 ? (
<div
style={{
textAlign: 'center',
padding: '2.5rem 1rem',
color: 'var(--color-text-muted)',
}}
>
<CheckCircle2
size={32}
style={{ color: 'var(--color-success)', marginBottom: 8 }}
aria-hidden="true"
/>
<p style={{ fontSize: '0.875rem', margin: 0 }}>All caught up</p>
<p style={{ fontSize: '0.75rem', margin: '4px 0 0', color: 'var(--color-text-muted)' }}>
No urgent action items right now
</p>
</div>
) : (
<ul
style={{
listStyle: 'none',
margin: 0,
padding: 0,
display: 'flex',
flexDirection: 'column',
gap: 4,
}}
role="list"
aria-label="Action items requiring attention"
>
{actionItems.map((item) => {
const Icon = item.icon;
return (
<li
key={item.id}
style={{
display: 'flex',
alignItems: 'center',
gap: 10,
padding: '0.625rem 0.75rem',
borderRadius: 'var(--radius-sm)',
transition: 'background-color var(--transition)',
cursor: 'default',
}}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = 'var(--color-surface-el)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = 'transparent';
}}
>
{/* Priority dot */}
<PriorityDot level={item.priority} />
{/* Type icon */}
<Icon
size={14}
style={{ color: 'var(--color-text-muted)', flexShrink: 0 }}
aria-hidden="true"
/>
{/* Text */}
<div style={{ flex: 1, minWidth: 0 }}>
<p
style={{
fontSize: '0.8125rem',
fontWeight: 500,
color: 'var(--color-text)',
margin: 0,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
title={item.title}
>
{item.title}
</p>
{item.subtitle && (
<p
style={{
fontSize: '0.7rem',
color: 'var(--color-text-muted)',
margin: '1px 0 0',
}}
>
{item.subtitle}
</p>
)}
</div>
{/* Action button */}
<button
type="button"
onClick={() => onNavigate?.(item.targetTab)}
className="btn btn-ghost btn-sm"
style={{
fontSize: '0.7rem',
padding: '2px 8px',
flexShrink: 0,
color: 'var(--color-primary)',
}}
aria-label={`${item.actionLabel}: ${item.title}`}
>
{item.actionLabel}
<ArrowRight size={11} aria-hidden="true" />
</button>
</li>
);
})}
</ul>
)}
</div>
</section>
{/* Right: Recent Activity */}
<section
className="card card-enter card-enter-2"
style={{ padding: 0, overflow: 'hidden' }}
aria-labelledby="recent-activity-heading"
>
{/* Header */}
<div
className="section-header"
style={{ padding: '0.875rem 1rem', marginBottom: 0 }}
>
<Activity size={16} style={{ color: 'var(--color-primary)' }} aria-hidden="true" />
<h2 id="recent-activity-heading" className="section-title" style={{ fontSize: '0.9375rem' }}>
Recent Activity
</h2>
</div>
{/* Body */}
<div
style={{
padding: '0.5rem 0.875rem 1rem',
overflowY: 'auto',
maxHeight: 420,
}}
>
{sectionsLoading.activity ? (
<SkeletonRows count={6} />
) : data.recentActivity.length === 0 ? (
<div
style={{
textAlign: 'center',
padding: '2rem 1rem',
color: 'var(--color-text-muted)',
}}
>
<Bell size={28} style={{ marginBottom: 8 }} aria-hidden="true" />
<p style={{ fontSize: '0.8125rem', margin: 0 }}>No recent activity</p>
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
{Object.entries(activityGroups).map(([group, items]) => {
if (items.length === 0) return null;
return (
<div key={group}>
{/* Group label */}
<p
style={{
fontSize: '0.65rem',
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '0.1em',
color: 'var(--color-text-muted)',
margin: '0 0 6px',
padding: '0 0.25rem',
}}
>
{group}
</p>
{/* Timeline items */}
<ul
style={{
listStyle: 'none',
margin: 0,
padding: 0,
display: 'flex',
flexDirection: 'column',
gap: 1,
}}
role="list"
>
{items.map((item) => {
const meta = ALERT_TYPE_META[item.type];
const color = meta?.color ?? '#6B7280';
const Icon = meta?.icon ?? Bell;
return (
<li
key={`${item.type}-${item.id}`}
style={{
display: 'flex',
alignItems: 'flex-start',
gap: 8,
padding: '5px 6px',
borderRadius: 'var(--radius-sm)',
transition: 'background-color var(--transition)',
}}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = 'var(--color-surface-el)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = 'transparent';
}}
>
{/* Colored dot with icon */}
<div
style={{
width: 22,
height: 22,
borderRadius: 6,
backgroundColor: `${color}18`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
marginTop: 1,
}}
>
<Icon size={11} style={{ color }} aria-hidden="true" />
</div>
{/* Text */}
<div style={{ flex: 1, minWidth: 0 }}>
<p
style={{
fontSize: '0.75rem',
fontWeight: 500,
color: 'var(--color-text)',
margin: 0,
lineHeight: 1.35,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
title={item.title}
>
{item.title}
</p>
<p
style={{
fontSize: '0.65rem',
color: 'var(--color-text-muted)',
margin: '1px 0 0',
}}
>
{relativeTime(item.date)}
</p>
</div>
</li>
);
})}
</ul>
</div>
);
})}
</div>
)}
</div>
</section>
</div>
{/* ── 4. Quick Actions Row ─────────────────────────────────────────── */}
<section
aria-label="Quick actions"
style={{
display: 'flex',
gap: '0.75rem',
flexWrap: 'wrap',
}}
>
{[
{
icon: Plus,
label: 'New Morning Batch',
tab: 'drafts',
accent: '#6366f1',
},
{
icon: Megaphone,
label: 'Create Petition',
tab: 'petition-creator',
accent: '#f43f5e',
},
{
icon: Search,
label: 'Find Grants',
tab: 'agent-grants',
accent: '#f59e0b',
},
{
icon: GitBranch,
label: 'View Pipeline',
tab: 'pipeline',
accent: '#10b981',
},
].map(({ icon: Icon, label, tab, accent }) => (
<button
key={tab}
type="button"
onClick={() => onNavigate?.(tab)}
className="btn btn-secondary hover-lift"
style={{
gap: 8,
fontSize: '0.8125rem',
border: `1px solid var(--color-border)`,
color: 'var(--color-text-secondary)',
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = accent;
e.currentTarget.style.color = accent;
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = 'var(--color-border)';
e.currentTarget.style.color = 'var(--color-text-secondary)';
}}
>
<Icon size={14} aria-hidden="true" />
{label}
</button>
))}
{/* AI Member Email button */}
<button
type="button"
onClick={() => setMemberEmailOpen(true)}
className="btn btn-secondary hover-lift"
style={{
gap: 8,
fontSize: '0.8125rem',
border: '1px solid var(--color-border)',
color: 'var(--color-text-secondary)',
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = '#06b6d4';
e.currentTarget.style.color = '#06b6d4';
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = 'var(--color-border)';
e.currentTarget.style.color = 'var(--color-text-secondary)';
}}
>
<Mail size={14} aria-hidden="true" />
Member Email
</button>
{/* Filler spacer for alignment */}
<div style={{ flex: 1 }} />
{/* Trending indicator */}
{!loading && data.activePetitionsCount > 0 && (
<span
className="badge badge-success"
style={{ alignSelf: 'center', gap: 4 }}
>
<TrendingUp size={11} aria-hidden="true" />
{data.activePetitionsCount} active
</span>
)}
</section>
{/* ── Member Email Modal ──────────────────────────────────────────────── */}
<MemberEmailModal
open={memberEmailOpen}
onClose={() => setMemberEmailOpen(false)}
/>
</div>
);
}