← back to Freddy

components/organizations/OrgsTab.tsx

331 lines

'use client';

import { useState, useEffect, useCallback } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { SkeletonList } from '../Skeleton';
import {
  Building2, Search, Plus, Star, Shield, Users,
  TrendingUp, ExternalLink, X, Loader2, MapPin,
} from 'lucide-react';
import { useToast } from '../ToastProvider';
import { useDebounce } from '@/hooks/useDebounce';

interface Org {
  id: string;
  name: string;
  ein: string | null;
  mission: string | null;
  description: string | null;
  website_url: string | null;
  city: string | null;
  state: string | null;
  category: string;
  focus_areas: string[];
  year_founded: number | null;
  annual_budget: string | null;
  staff_size: number | null;
  impact_score: number;
  trust_score: number;
  total_votes: number;
  total_funding_received: string;
  status: string;
  created_at: string;
}

const CATEGORIES = [
  'all', 'education', 'health', 'environment', 'poverty', 'housing',
  'legal_aid', 'civil_rights', 'veterans', 'youth', 'elderly',
  'disability', 'arts', 'other',
];

const fadeIn = {
  hidden: { opacity: 0, y: 20 },
  visible: (i: number) => ({
    opacity: 1, y: 0,
    transition: { delay: i * 0.04, duration: 0.35 },
  }),
};

export default function OrgsTab() {
  const { addToast } = useToast();
  const [orgs, setOrgs] = useState<Org[]>([]);
  const [loading, setLoading] = useState(true);
  const [sortBy, setSortBy] = useState('votes');
  const [category, setCategory] = useState('all');
  const [searchTerm, setSearchTerm] = useState('');
  const debouncedSearch = useDebounce(searchTerm, 300);
  const [showCreate, setShowCreate] = useState(false);

  const fetchOrgs = useCallback(async () => {
    try {
      const params = new URLSearchParams({ sort: sortBy });
      if (category !== 'all') params.set('category', category);
      const res = await fetch(`/api/organizations?${params}`, { credentials: 'include' });
      if (res.ok) {
        const data = await res.json();
        setOrgs(data.organizations || []);
      }
    } catch (err) {
      console.error('Failed to fetch orgs:', err);
    } finally {
      setLoading(false);
    }
  }, [sortBy, category]);

  useEffect(() => { fetchOrgs(); }, [fetchOrgs]);

  const handleCreate = async (formData: Record<string, string>) => {
    try {
      const res = await fetch('/api/organizations', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        credentials: 'include',
        body: JSON.stringify({
          ...formData,
          focus_areas: formData.focus_areas ? formData.focus_areas.split(',').map(s => s.trim()).filter(Boolean) : [],
          year_founded: formData.year_founded ? parseInt(formData.year_founded) : null,
          staff_size: formData.staff_size ? parseInt(formData.staff_size) : null,
        }),
      });
      if (res.ok) {
        setShowCreate(false);
        await fetchOrgs();
        addToast('Organization registered', 'success');
      } else {
        addToast('Failed to register organization', 'error');
      }
    } catch (err) {
      console.error('Create org error:', err);
      addToast('Failed to register organization', 'error');
    }
  };

  const filtered = orgs.filter(o =>
    !debouncedSearch || o.name.toLowerCase().includes(debouncedSearch.toLowerCase()) ||
    (o.mission && o.mission.toLowerCase().includes(debouncedSearch.toLowerCase()))
  );

  const statusColor = (s: string) => {
    if (s === 'featured') return 'badge-amber';
    if (s === 'verified') return 'badge-success';
    return 'badge-warning';
  };

  return (
    <div className="p-6 space-y-6">
      <div className="flex items-center justify-between flex-wrap gap-3">
        <div>
          <h2 className="text-xl font-bold flex items-center gap-2" style={{ color: 'var(--color-text)' }}>
            <Building2 size={20} style={{ color: '#22c55e' }} />
            Organizations
          </h2>
          <p className="text-sm mt-1" style={{ color: 'var(--color-text-muted)' }}>
            Non-profit registry &mdash; verified organizations addressing top causes
          </p>
        </div>
        <button onClick={() => setShowCreate(true)} className="btn btn-primary">
          <Plus size={14} />
          Register Organization
        </button>
      </div>

      <div className="flex items-center gap-3 flex-wrap">
        <div className="relative flex-1 max-w-xs">
          <Search size={14} style={{ position: 'absolute', left: 10, top: '50%', transform: 'translateY(-50%)', color: 'var(--color-text-muted)' }} />
          <input className="input pl-8" placeholder="Search organizations..." value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} />
        </div>
        <select className="input" style={{ width: 'auto' }} value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
          <option value="votes">Sort by Votes</option>
          <option value="impact">Sort by Impact</option>
          <option value="trust">Sort by Trust</option>
          <option value="funding">Sort by Funding</option>
        </select>
      </div>

      <div className="flex items-center gap-2 flex-wrap">
        {CATEGORIES.map((cat) => (
          <button
            key={cat}
            onClick={() => setCategory(cat)}
            className="btn btn-sm"
            style={{
              backgroundColor: category === cat ? 'rgba(217, 119, 6, 0.2)' : 'var(--color-surface-el)',
              color: category === cat ? '#fbbf24' : 'var(--color-text-secondary)',
              borderColor: category === cat ? 'rgba(217, 119, 6, 0.3)' : 'var(--color-border)',
            }}
          >
            {cat === 'all' ? 'All' : cat.replace('_', ' ')}
          </button>
        ))}
      </div>

      {loading ? (
        <SkeletonList count={4} />
      ) : (
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
          <AnimatePresence>
            {filtered.map((org, i) => (
              <motion.div key={org.id} custom={i} initial="hidden" animate="visible" variants={fadeIn} className="card p-4 flex flex-col gap-3">
                <div className="flex items-start justify-between">
                  <div className="flex-1 min-w-0">
                    <h3 className="text-sm font-semibold truncate" style={{ color: 'var(--color-text)' }}>
                      {org.name}
                    </h3>
                    <div className="flex items-center gap-2 mt-1">
                      <span className={`badge ${statusColor(org.status)} text-xs`}>{org.status}</span>
                      {org.category && <span className="text-xs" style={{ color: 'var(--color-text-muted)' }}>{org.category.replace('_', ' ')}</span>}
                    </div>
                  </div>
                  {org.website_url && (
                    <a href={org.website_url} target="_blank" rel="noopener noreferrer" className="btn btn-ghost btn-sm">
                      <ExternalLink size={12} />
                    </a>
                  )}
                </div>

                {org.mission && (
                  <p className="text-xs line-clamp-2" style={{ color: 'var(--color-text-muted)' }}>{org.mission}</p>
                )}

                {(org.city || org.state) && (
                  <div className="flex items-center gap-1 text-xs" style={{ color: 'var(--color-text-muted)' }}>
                    <MapPin size={10} /> {[org.city, org.state].filter(Boolean).join(', ')}
                  </div>
                )}

                <div className="grid grid-cols-3 gap-2">
                  <div className="text-center p-2 rounded-lg" style={{ backgroundColor: 'var(--color-surface-el)' }}>
                    <Shield size={12} style={{ color: '#60a5fa', margin: '0 auto 2px' }} />
                    <div className="text-xs font-bold" style={{ color: 'var(--color-text)' }}>{((org.trust_score || 0) * 100).toFixed(0)}%</div>
                    <div className="text-xs" style={{ color: 'var(--color-text-muted)' }}>Trust</div>
                  </div>
                  <div className="text-center p-2 rounded-lg" style={{ backgroundColor: 'var(--color-surface-el)' }}>
                    <TrendingUp size={12} style={{ color: '#22c55e', margin: '0 auto 2px' }} />
                    <div className="text-xs font-bold" style={{ color: 'var(--color-text)' }}>{((org.impact_score || 0) * 100).toFixed(0)}%</div>
                    <div className="text-xs" style={{ color: 'var(--color-text-muted)' }}>Impact</div>
                  </div>
                  <div className="text-center p-2 rounded-lg" style={{ backgroundColor: 'var(--color-surface-el)' }}>
                    <Users size={12} style={{ color: '#fbbf24', margin: '0 auto 2px' }} />
                    <div className="text-xs font-bold" style={{ color: 'var(--color-text)' }}>{org.total_votes}</div>
                    <div className="text-xs" style={{ color: 'var(--color-text-muted)' }}>Votes</div>
                  </div>
                </div>

                {org.focus_areas && org.focus_areas.length > 0 && (
                  <div className="flex items-center gap-1 flex-wrap">
                    {org.focus_areas.slice(0, 3).map((area: string) => (
                      <span key={area} className="text-xs px-2 py-0.5 rounded-full" style={{ backgroundColor: 'var(--color-surface-el)', color: 'var(--color-text-muted)', border: '1px solid var(--color-border)' }}>
                        {area}
                      </span>
                    ))}
                  </div>
                )}
              </motion.div>
            ))}
          </AnimatePresence>
        </div>
      )}

      {!loading && filtered.length === 0 && (
        <div className="text-center py-12">
          <Building2 size={32} style={{ color: 'var(--color-text-muted)', margin: '0 auto 12px' }} />
          <p style={{ color: 'var(--color-text-muted)' }}>No organizations found. Register one to get started.</p>
        </div>
      )}

      {showCreate && <CreateOrgModal onClose={() => setShowCreate(false)} onSubmit={handleCreate} />}
    </div>
  );
}

function CreateOrgModal({ onClose, onSubmit }: { onClose: () => void; onSubmit: (data: Record<string, string>) => void }) {
  useEffect(() => {
    const handleKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
    window.addEventListener('keydown', handleKey);
    return () => window.removeEventListener('keydown', handleKey);
  }, [onClose]);

  const [form, setForm] = useState({ name: '', ein: '', mission: '', description: '', website_url: '', city: '', state: '', category: 'other', focus_areas: '', year_founded: '', annual_budget: '', staff_size: '' });
  const [submitting, setSubmitting] = useState(false);

  const handleSubmit = async () => {
    setSubmitting(true);
    await onSubmit(form);
    setSubmitting(false);
  };

  const set = (k: string) => (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => setForm(f => ({ ...f, [k]: e.target.value }));

  return (
    <div className="modal-overlay" onClick={onClose}>
      <motion.div initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} className="modal-content" onClick={(e) => e.stopPropagation()}>
        <div className="flex items-center justify-between mb-4">
          <h3 className="text-lg font-semibold" style={{ color: 'var(--color-text)' }}>Register Organization</h3>
          <button onClick={onClose} className="btn btn-ghost btn-sm"><X size={16} /></button>
        </div>
        <div className="space-y-3">
          <div>
            <label className="block text-sm mb-1" style={{ color: 'var(--color-text-secondary)' }}>Name *</label>
            <input className="input" value={form.name} onChange={set('name')} placeholder="Organization name" />
          </div>
          <div className="grid grid-cols-2 gap-3">
            <div>
              <label className="block text-sm mb-1" style={{ color: 'var(--color-text-secondary)' }}>EIN</label>
              <input className="input" value={form.ein} onChange={set('ein')} placeholder="XX-XXXXXXX" />
            </div>
            <div>
              <label className="block text-sm mb-1" style={{ color: 'var(--color-text-secondary)' }}>Category</label>
              <select className="input" value={form.category} onChange={set('category')}>
                {CATEGORIES.filter(c => c !== 'all').map(c => <option key={c} value={c}>{c.replace('_', ' ')}</option>)}
              </select>
            </div>
          </div>
          <div>
            <label className="block text-sm mb-1" style={{ color: 'var(--color-text-secondary)' }}>Mission</label>
            <textarea className="input" rows={2} value={form.mission} onChange={set('mission')} placeholder="Organization mission..." />
          </div>
          <div>
            <label className="block text-sm mb-1" style={{ color: 'var(--color-text-secondary)' }}>Website</label>
            <input className="input" value={form.website_url} onChange={set('website_url')} placeholder="https://..." />
          </div>
          <div className="grid grid-cols-2 gap-3">
            <div>
              <label className="block text-sm mb-1" style={{ color: 'var(--color-text-secondary)' }}>City</label>
              <input className="input" value={form.city} onChange={set('city')} />
            </div>
            <div>
              <label className="block text-sm mb-1" style={{ color: 'var(--color-text-secondary)' }}>State</label>
              <input className="input" value={form.state} onChange={set('state')} />
            </div>
          </div>
          <div>
            <label className="block text-sm mb-1" style={{ color: 'var(--color-text-secondary)' }}>Focus Areas (comma-separated)</label>
            <input className="input" value={form.focus_areas} onChange={set('focus_areas')} placeholder="education, youth, mentoring" />
          </div>
          <div className="grid grid-cols-3 gap-3">
            <div>
              <label className="block text-sm mb-1" style={{ color: 'var(--color-text-secondary)' }}>Year Founded</label>
              <input className="input" type="number" value={form.year_founded} onChange={set('year_founded')} />
            </div>
            <div>
              <label className="block text-sm mb-1" style={{ color: 'var(--color-text-secondary)' }}>Annual Budget</label>
              <input className="input" value={form.annual_budget} onChange={set('annual_budget')} placeholder="$500K" />
            </div>
            <div>
              <label className="block text-sm mb-1" style={{ color: 'var(--color-text-secondary)' }}>Staff Size</label>
              <input className="input" type="number" value={form.staff_size} onChange={set('staff_size')} />
            </div>
          </div>
          <div className="flex justify-end gap-2 pt-2">
            <button onClick={onClose} className="btn btn-secondary">Cancel</button>
            <button onClick={handleSubmit} disabled={!form.name || submitting} className="btn btn-primary">
              {submitting ? <Loader2 size={14} className="animate-spin" /> : null}
              Register
            </button>
          </div>
        </div>
      </motion.div>
    </div>
  );
}