← back to Norma
components/agents/OrgOnboarding.tsx
444 lines
'use client';
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
UserPlus, Building2, Globe, Mail, Phone, MapPin, Users, CheckCircle2,
ChevronRight, Twitter, Linkedin, Facebook, Instagram, AlertCircle,
} from 'lucide-react';
import { useToast } from '../ToastProvider';
const ORG_TYPES = [
{ value: 'nonprofit', label: 'Nonprofit' },
{ value: 'advocacy', label: 'Advocacy' },
{ value: 'educational', label: 'Educational' },
{ value: 'government', label: 'Government' },
];
const KEY_ISSUES = [
'Education', 'Student Loans', 'Workforce Development', 'Housing',
'Healthcare', 'Economic Justice', 'Racial Equity', 'Climate',
'Immigration', 'Voting Rights', 'Criminal Justice', 'Food Security',
'Mental Health', 'Child Welfare', 'Disability Rights', 'Elder Care',
];
const FOCUS_POPULATIONS = [
'Students', 'Veterans', 'Minorities', 'Low-Income',
'Women', 'LGBTQ+', 'Immigrants', 'Seniors',
'Youth', 'Rural Communities', 'Urban Communities', 'People with Disabilities',
];
const US_STATES = [
'AL','AK','AZ','AR','CA','CO','CT','DE','FL','GA','HI','ID','IL','IN','IA','KS','KY',
'LA','ME','MD','MA','MI','MN','MS','MO','MT','NE','NV','NH','NJ','NM','NY','NC','ND',
'OH','OK','OR','PA','RI','SC','SD','TN','TX','UT','VT','VA','WA','WV','WI','WY','DC',
];
interface OnboardingProps {
onNavigate: (tab: string) => void;
onOrgChange: (id: string) => void;
}
export default function OrgOnboarding({ onNavigate, onOrgChange }: OnboardingProps) {
const { addToast } = useToast();
const [submitting, setSubmitting] = useState(false);
const [submitted, setSubmitted] = useState(false);
const [newOrgName, setNewOrgName] = useState('');
const [errors, setErrors] = useState<Record<string, string>>({});
// Form state
const [form, setForm] = useState({
name: '',
ein: '',
type: 'nonprofit',
mission: '',
key_issues: [] as string[],
focus_populations: [] as string[],
city: '',
state: '',
zip: '',
website: '',
email: '',
phone: '',
executive_director: '',
twitter: '',
facebook: '',
instagram: '',
linkedin: '',
});
function updateField(field: string, value: string) {
setForm(prev => ({ ...prev, [field]: value }));
if (errors[field]) setErrors(prev => { const n = { ...prev }; delete n[field]; return n; });
}
function toggleArrayItem(field: 'key_issues' | 'focus_populations', item: string) {
setForm(prev => ({
...prev,
[field]: prev[field].includes(item)
? prev[field].filter(i => i !== item)
: [...prev[field], item],
}));
}
function validate(): boolean {
const errs: Record<string, string> = {};
if (!form.name.trim()) errs.name = 'Organization name is required';
if (!form.email.trim()) errs.email = 'Contact email is required';
if (form.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email)) errs.email = 'Invalid email format';
if (form.key_issues.length === 0) errs.key_issues = 'Select at least one issue';
setErrors(errs);
return Object.keys(errs).length === 0;
}
async function handleSubmit() {
if (!validate()) return;
setSubmitting(true);
try {
const payload = {
name: form.name.trim(),
ein: form.ein.trim() || null,
type: form.type,
mission: form.mission.trim() || null,
key_issues: form.key_issues,
focus_populations: form.focus_populations,
city: form.city.trim() || null,
state: form.state || null,
zip: form.zip.trim() || null,
website: form.website.trim() || null,
email: form.email.trim(),
phone: form.phone.trim() || null,
executive_director: form.executive_director.trim() || null,
social_media: {
twitter: form.twitter.trim() || null,
facebook: form.facebook.trim() || null,
instagram: form.instagram.trim() || null,
linkedin: form.linkedin.trim() || null,
},
};
const res = await fetch('/api/agents/org/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!res.ok) {
const data = await res.json();
throw new Error(data.error || 'Registration failed');
}
const data = await res.json();
setNewOrgName(form.name);
setSubmitted(true);
if (data.id) onOrgChange(data.id);
addToast('Organization registered successfully', 'success');
} catch (err) {
addToast((err as Error).message || 'Failed to register organization', 'error');
}
setSubmitting(false);
}
if (submitted) {
return (
<div style={{ padding: 24, maxWidth: 700, margin: '0 auto' }}>
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3 }}
>
<div className="card" style={{ padding: 40, textAlign: 'center' }}>
<div style={{
width: 64, height: 64, borderRadius: 16, margin: '0 auto 20px',
backgroundColor: 'rgba(34,197,94,0.15)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<CheckCircle2 size={32} style={{ color: '#22c55e' }} />
</div>
<h2 style={{ fontSize: 24, fontWeight: 700, color: 'var(--color-text)', margin: '0 0 8px' }}>
Your Agent Toolkit is Ready
</h2>
<p style={{ fontSize: 14, color: 'var(--color-text-secondary)', margin: '0 0 28px' }}>
<strong>{newOrgName}</strong> has been registered. Explore your AI-powered advocacy tools.
</p>
<div style={{
display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',
gap: 12, marginBottom: 20,
}}>
{[
{ tab: 'agent-petition', label: 'Petition Builder', icon: '📋' },
{ tab: 'agent-grants', label: 'Grant Finder', icon: '🔍' },
{ tab: 'agent-advocacy', label: 'Advocacy Toolkit', icon: '🎯' },
{ tab: 'agent-hub', label: 'Agent Hub', icon: '🤖' },
].map(tool => (
<button
key={tool.tab}
className="btn btn-secondary"
onClick={() => onNavigate(tool.tab)}
style={{ justifyContent: 'flex-start', gap: 8 }}
>
<span>{tool.icon}</span>
{tool.label}
<ChevronRight size={14} style={{ marginLeft: 'auto' }} />
</button>
))}
</div>
</div>
</motion.div>
</div>
);
}
return (
<div style={{ padding: 24, maxWidth: 800, margin: '0 auto' }}>
{/* Header */}
<div style={{ marginBottom: 24 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 8 }}>
<div style={{
width: 40, height: 40, borderRadius: 12,
backgroundColor: 'rgba(34,197,94,0.12)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<UserPlus size={20} style={{ color: '#22c55e' }} />
</div>
<div>
<h1 style={{ fontSize: 22, fontWeight: 700, color: 'var(--color-text)', margin: 0 }}>
Organization Onboarding
</h1>
<p style={{ fontSize: 13, color: 'var(--color-text-muted)', margin: 0 }}>
Register your nonprofit to access AI-powered advocacy tools
</p>
</div>
</div>
</div>
{/* Form */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
{/* Basic Info */}
<div className="card" style={{ padding: 20 }}>
<h3 style={{ fontSize: 14, fontWeight: 600, color: 'var(--color-text)', margin: '0 0 16px', display: 'flex', alignItems: 'center', gap: 8 }}>
<Building2 size={16} style={{ color: 'var(--color-primary)' }} />
Organization Details
</h3>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
<div style={{ gridColumn: '1 / -1' }}>
<label style={{ fontSize: 12, fontWeight: 600, color: 'var(--color-text-secondary)', display: 'block', marginBottom: 4 }}>
Organization Name *
</label>
<input
className="input"
value={form.name}
onChange={e => updateField('name', e.target.value)}
placeholder="e.g., Your Organization Name"
/>
{errors.name && <span style={{ fontSize: 11, color: 'var(--color-error)', marginTop: 2, display: 'flex', alignItems: 'center', gap: 4 }}><AlertCircle size={11} />{errors.name}</span>}
</div>
<div>
<label style={{ fontSize: 12, fontWeight: 600, color: 'var(--color-text-secondary)', display: 'block', marginBottom: 4 }}>
EIN (Tax ID)
</label>
<input
className="input"
value={form.ein}
onChange={e => updateField('ein', e.target.value)}
placeholder="XX-XXXXXXX"
/>
</div>
<div>
<label style={{ fontSize: 12, fontWeight: 600, color: 'var(--color-text-secondary)', display: 'block', marginBottom: 4 }}>
Organization Type
</label>
<select
className="input"
value={form.type}
onChange={e => updateField('type', e.target.value)}
style={{ cursor: 'pointer' }}
>
{ORG_TYPES.map(t => <option key={t.value} value={t.value}>{t.label}</option>)}
</select>
</div>
<div style={{ gridColumn: '1 / -1' }}>
<label style={{ fontSize: 12, fontWeight: 600, color: 'var(--color-text-secondary)', display: 'block', marginBottom: 4 }}>
Mission Statement
</label>
<textarea
className="input"
value={form.mission}
onChange={e => updateField('mission', e.target.value)}
placeholder="Briefly describe your organization's mission..."
rows={3}
style={{ resize: 'vertical', fontFamily: 'inherit' }}
/>
</div>
<div>
<label style={{ fontSize: 12, fontWeight: 600, color: 'var(--color-text-secondary)', display: 'block', marginBottom: 4 }}>
Executive Director
</label>
<input
className="input"
value={form.executive_director}
onChange={e => updateField('executive_director', e.target.value)}
placeholder="Full name"
/>
</div>
</div>
</div>
{/* Key Issues */}
<div className="card" style={{ padding: 20 }}>
<h3 style={{ fontSize: 14, fontWeight: 600, color: 'var(--color-text)', margin: '0 0 4px', display: 'flex', alignItems: 'center', gap: 8 }}>
<Users size={16} style={{ color: '#f59e0b' }} />
Key Issues & Focus Populations
</h3>
<p style={{ fontSize: 12, color: 'var(--color-text-muted)', margin: '0 0 14px' }}>
Select all that apply to your organization.
</p>
<label style={{ fontSize: 12, fontWeight: 600, color: 'var(--color-text-secondary)', display: 'block', marginBottom: 8 }}>
Key Issues *
</label>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginBottom: 16 }}>
{KEY_ISSUES.map(issue => {
const selected = form.key_issues.includes(issue);
return (
<button
key={issue}
type="button"
onClick={() => toggleArrayItem('key_issues', issue)}
style={{
padding: '5px 12px', borderRadius: 20, border: '1px solid',
borderColor: selected ? 'var(--color-primary)' : 'var(--color-border)',
backgroundColor: selected ? 'rgba(16,185,129,0.15)' : 'transparent',
color: selected ? '#34d399' : 'var(--color-text-secondary)',
fontSize: 12, fontWeight: 500, cursor: 'pointer',
transition: 'all 0.15s',
}}
>
{issue}
</button>
);
})}
</div>
{errors.key_issues && <span style={{ fontSize: 11, color: 'var(--color-error)', display: 'flex', alignItems: 'center', gap: 4, marginBottom: 12 }}><AlertCircle size={11} />{errors.key_issues}</span>}
<label style={{ fontSize: 12, fontWeight: 600, color: 'var(--color-text-secondary)', display: 'block', marginBottom: 8 }}>
Focus Populations
</label>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
{FOCUS_POPULATIONS.map(pop => {
const selected = form.focus_populations.includes(pop);
return (
<button
key={pop}
type="button"
onClick={() => toggleArrayItem('focus_populations', pop)}
style={{
padding: '5px 12px', borderRadius: 20, border: '1px solid',
borderColor: selected ? '#8b5cf6' : 'var(--color-border)',
backgroundColor: selected ? 'rgba(139,92,246,0.15)' : 'transparent',
color: selected ? '#a78bfa' : 'var(--color-text-secondary)',
fontSize: 12, fontWeight: 500, cursor: 'pointer',
transition: 'all 0.15s',
}}
>
{pop}
</button>
);
})}
</div>
</div>
{/* Contact Info */}
<div className="card" style={{ padding: 20 }}>
<h3 style={{ fontSize: 14, fontWeight: 600, color: 'var(--color-text)', margin: '0 0 16px', display: 'flex', alignItems: 'center', gap: 8 }}>
<MapPin size={16} style={{ color: '#22c55e' }} />
Contact Information
</h3>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 12 }}>
<div>
<label style={{ fontSize: 12, fontWeight: 600, color: 'var(--color-text-secondary)', display: 'block', marginBottom: 4 }}>City</label>
<input className="input" value={form.city} onChange={e => updateField('city', e.target.value)} placeholder="City" />
</div>
<div>
<label style={{ fontSize: 12, fontWeight: 600, color: 'var(--color-text-secondary)', display: 'block', marginBottom: 4 }}>State</label>
<select className="input" value={form.state} onChange={e => updateField('state', e.target.value)} style={{ cursor: 'pointer' }}>
<option value="">Select</option>
{US_STATES.map(s => <option key={s} value={s}>{s}</option>)}
</select>
</div>
<div>
<label style={{ fontSize: 12, fontWeight: 600, color: 'var(--color-text-secondary)', display: 'block', marginBottom: 4 }}>ZIP</label>
<input className="input" value={form.zip} onChange={e => updateField('zip', e.target.value)} placeholder="ZIP Code" />
</div>
<div>
<label style={{ fontSize: 12, fontWeight: 600, color: 'var(--color-text-secondary)', display: 'block', marginBottom: 4 }}>Email *</label>
<input className="input" type="email" value={form.email} onChange={e => updateField('email', e.target.value)} placeholder="contact@org.com" />
{errors.email && <span style={{ fontSize: 11, color: 'var(--color-error)', marginTop: 2, display: 'flex', alignItems: 'center', gap: 4 }}><AlertCircle size={11} />{errors.email}</span>}
</div>
<div>
<label style={{ fontSize: 12, fontWeight: 600, color: 'var(--color-text-secondary)', display: 'block', marginBottom: 4 }}>Phone</label>
<input className="input" value={form.phone} onChange={e => updateField('phone', e.target.value)} placeholder="(555) 123-4567" />
</div>
<div>
<label style={{ fontSize: 12, fontWeight: 600, color: 'var(--color-text-secondary)', display: 'block', marginBottom: 4 }}>Website</label>
<input className="input" value={form.website} onChange={e => updateField('website', e.target.value)} placeholder="https://www.org.com" />
</div>
</div>
</div>
{/* Social Media */}
<div className="card" style={{ padding: 20 }}>
<h3 style={{ fontSize: 14, fontWeight: 600, color: 'var(--color-text)', margin: '0 0 16px', display: 'flex', alignItems: 'center', gap: 8 }}>
<Globe size={16} style={{ color: '#6366f1' }} />
Social Media Links
</h3>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Twitter size={16} style={{ color: '#34d399', flexShrink: 0 }} />
<input className="input" value={form.twitter} onChange={e => updateField('twitter', e.target.value)} placeholder="https://twitter.com/yourorg" />
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Facebook size={16} style={{ color: '#34d399', flexShrink: 0 }} />
<input className="input" value={form.facebook} onChange={e => updateField('facebook', e.target.value)} placeholder="https://facebook.com/yourorg" />
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Instagram size={16} style={{ color: '#e879f9', flexShrink: 0 }} />
<input className="input" value={form.instagram} onChange={e => updateField('instagram', e.target.value)} placeholder="https://instagram.com/yourorg" />
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Linkedin size={16} style={{ color: '#34d399', flexShrink: 0 }} />
<input className="input" value={form.linkedin} onChange={e => updateField('linkedin', e.target.value)} placeholder="https://linkedin.com/company/yourorg" />
</div>
</div>
</div>
{/* Submit */}
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 12 }}>
<button className="btn btn-ghost" onClick={() => onNavigate('agent-hub')}>
Cancel
</button>
<button
className="btn btn-primary btn-lg"
onClick={handleSubmit}
disabled={submitting}
>
{submitting ? (
<>
<span className="spinner" style={{ width: 16, height: 16 }} />
Registering...
</>
) : (
<>
<UserPlus size={16} />
Register Organization
</>
)}
</button>
</div>
</div>
</div>
);
}