← back to Norma
components/agents/AgentAppsHub.tsx
230 lines
'use client';
import { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import {
Bot, ScrollText, Search, Target, UserPlus, ChevronDown, Building2, Sparkles,
} from 'lucide-react';
interface Organization {
id: string;
name: string;
city: string | null;
state: string | null;
type: string;
}
interface AgentAppsHubProps {
onNavigate: (tab: string) => void;
selectedOrgId: string;
onOrgChange: (id: string) => void;
}
const TOOLS = [
{
id: 'agent-onboard',
title: 'Organization Onboarding',
description: 'Register your nonprofit organization to unlock the full advocacy agent toolkit. Set up your mission, key issues, and contact information.',
Icon: UserPlus,
color: '#22c55e',
bg: 'rgba(34,197,94,0.12)',
borderColor: 'rgba(34,197,94,0.25)',
},
{
id: 'agent-petition',
title: 'Petition Builder',
description: 'AI-assisted petition creation targeting Congress members, state legislatures, federal agencies, and institutions. Generate drafts, talking points, and signature goals.',
Icon: ScrollText,
color: '#6366f1',
bg: 'rgba(99,102,241,0.12)',
borderColor: 'rgba(99,102,241,0.25)',
},
{
id: 'agent-grants',
title: 'Grant Finder',
description: 'AI-powered grant matching that scores federal, state, foundation, and corporate grants against your organization\'s mission and focus areas.',
Icon: Search,
color: '#f59e0b',
bg: 'rgba(245,158,11,0.12)',
borderColor: 'rgba(245,158,11,0.25)',
},
{
id: 'agent-advocacy',
title: 'Advocacy Toolkit',
description: 'Generate complete advocacy campaigns with talking points, email templates, social media kits, and call scripts for grassroots organizing.',
Icon: Target,
color: '#8b5cf6',
bg: 'rgba(139,92,246,0.12)',
borderColor: 'rgba(139,92,246,0.25)',
},
];
export default function AgentAppsHub({ onNavigate, selectedOrgId, onOrgChange }: AgentAppsHubProps) {
const [orgs, setOrgs] = useState<Organization[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
(async () => {
try {
const res = await fetch('/api/orgs?limit=200&sort=name&sort_dir=asc');
if (res.ok) {
const data = await res.json();
setOrgs(data.rows || []);
}
} catch {
// silently fail
}
setLoading(false);
})();
}, []);
return (
<div style={{ padding: 24, maxWidth: 1100, margin: '0 auto' }}>
{/* Header */}
<div style={{ textAlign: 'center', marginBottom: 32 }}>
<div style={{
width: 56, height: 56, borderRadius: 16, margin: '0 auto 16px',
background: 'linear-gradient(135deg, #10b981, #7c3aed)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
boxShadow: '0 4px 20px rgba(16,185,129,0.3)',
}}>
<Bot size={28} style={{ color: '#fff' }} />
</div>
<h1 style={{ fontSize: 28, fontWeight: 700, color: 'var(--color-text)', margin: '0 0 8px' }}>
Norma Advocacy Agent Apps
</h1>
<p style={{ fontSize: 15, color: 'var(--color-text-secondary)', maxWidth: 600, margin: '0 auto' }}>
AI-powered tools for nonprofit organizations. Select your organization to get started.
</p>
</div>
{/* Organization Selector */}
<div className="card" style={{
marginBottom: 28, padding: 16,
display: 'flex', alignItems: 'center', gap: 12,
flexWrap: 'wrap',
}}>
<Building2 size={18} style={{ color: 'var(--color-primary)', flexShrink: 0 }} />
<label style={{ fontSize: 13, fontWeight: 600, color: 'var(--color-text)', flexShrink: 0 }}>
Active Organization:
</label>
<div style={{ position: 'relative', flex: 1, minWidth: 240 }}>
<select
className="input"
value={selectedOrgId}
onChange={e => onOrgChange(e.target.value)}
aria-label="Active organization"
style={{ paddingRight: 32, cursor: 'pointer' }}
disabled={loading}
>
<option value="">-- Select an organization --</option>
{orgs.map(o => (
<option key={o.id} value={o.id}>
{o.name}{o.city ? ` (${o.city}, ${o.state || ''})` : ''}
</option>
))}
</select>
<ChevronDown size={14} style={{
position: 'absolute', right: 10, top: '50%', transform: 'translateY(-50%)',
color: 'var(--color-text-muted)', pointerEvents: 'none',
}} />
</div>
{!selectedOrgId && (
<button
className="btn btn-primary btn-sm"
onClick={() => onNavigate('agent-onboard')}
>
<UserPlus size={14} />
Register New Org
</button>
)}
</div>
{/* Tools Grid */}
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))',
gap: 20,
}}>
{TOOLS.map((tool, idx) => (
<motion.div
key={tool.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: idx * 0.08, duration: 0.3 }}
>
<div
className="card"
style={{
padding: 24,
cursor: 'pointer',
borderColor: 'var(--color-border)',
transition: 'border-color 0.2s, box-shadow 0.2s, transform 0.2s',
height: '100%',
display: 'flex',
flexDirection: 'column',
}}
onMouseEnter={e => {
(e.currentTarget as HTMLElement).style.borderColor = tool.borderColor;
(e.currentTarget as HTMLElement).style.boxShadow = `0 4px 20px ${tool.bg}`;
(e.currentTarget as HTMLElement).style.transform = 'translateY(-2px)';
}}
onMouseLeave={e => {
(e.currentTarget as HTMLElement).style.borderColor = 'var(--color-border)';
(e.currentTarget as HTMLElement).style.boxShadow = 'none';
(e.currentTarget as HTMLElement).style.transform = 'translateY(0)';
}}
onClick={() => onNavigate(tool.id)}
>
<div style={{
width: 44, height: 44, borderRadius: 12,
backgroundColor: tool.bg,
display: 'flex', alignItems: 'center', justifyContent: 'center',
marginBottom: 16,
}}>
<tool.Icon size={22} style={{ color: tool.color }} />
</div>
<h3 style={{ fontSize: 16, fontWeight: 600, color: 'var(--color-text)', margin: '0 0 8px' }}>
{tool.title}
</h3>
<p style={{
fontSize: 13, color: 'var(--color-text-secondary)',
lineHeight: 1.6, margin: '0 0 20px', flex: 1,
}}>
{tool.description}
</p>
<button className="btn btn-primary btn-sm" style={{ alignSelf: 'flex-start' }}>
<Sparkles size={14} />
Launch
</button>
</div>
</motion.div>
))}
</div>
{/* Stats footer */}
<div style={{
marginTop: 32, padding: '16px 20px', borderRadius: 12,
backgroundColor: 'var(--color-surface-el)',
border: '1px solid var(--color-border)',
display: 'flex', justifyContent: 'center', gap: 40, flexWrap: 'wrap',
}}>
{[
{ label: 'Organizations', value: orgs.length.toString() },
{ label: 'Agent Tools', value: '4' },
{ label: 'AI Model', value: 'Gemini 2.0 Flash' },
].map(stat => (
<div key={stat.label} style={{ textAlign: 'center' }}>
<div style={{ fontSize: 20, fontWeight: 700, color: 'var(--color-primary)' }}>
{stat.value}
</div>
<div style={{ fontSize: 11, color: 'var(--color-text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
{stat.label}
</div>
</div>
))}
</div>
</div>
);
}