← back to Freddy

components/causes/CausesTab.tsx

388 lines

'use client';

import { useState, useEffect, useCallback } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { SkeletonList } from '../Skeleton';
import {
  Flame, Search, Plus, Sparkles, Loader2, TrendingUp,
  ArrowUpRight, Tag, Users, X,
} from 'lucide-react';
import { useToast } from '../ToastProvider';
import { useDebounce } from '@/hooks/useDebounce';

interface Cause {
  id: string;
  title: string;
  description: string;
  category: string;
  urgency_score: number;
  petition_velocity: number;
  news_mentions: number;
  total_votes: number;
  total_supporters: number;
  trending_rank: number | null;
  source: string;
  tags: string[];
  featured: boolean;
  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 CausesTab() {
  const { addToast } = useToast();
  const [causes, setCauses] = useState<Cause[]>([]);
  const [loading, setLoading] = useState(true);
  const [discovering, setDiscovering] = useState(false);
  const [sortBy, setSortBy] = useState('urgency');
  const [category, setCategory] = useState('all');
  const [searchTerm, setSearchTerm] = useState('');
  const debouncedSearch = useDebounce(searchTerm, 300);
  const [showCreate, setShowCreate] = useState(false);

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

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

  const handleDiscover = async () => {
    setDiscovering(true);
    try {
      const res = await fetch('/api/causes/discover', {
        method: 'POST',
        credentials: 'include',
      });
      if (res.ok) {
        await fetchCauses();
        addToast('Causes discovered', 'success');
      } else {
        addToast('Discovery failed', 'error');
      }
    } catch (err) {
      console.error('Discover error:', err);
      addToast('Discovery failed', 'error');
    } finally {
      setDiscovering(false);
    }
  };

  const handleCreateCause = async (formData: { title: string; description: string; category: string; tags: string }) => {
    try {
      const res = await fetch('/api/causes', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        credentials: 'include',
        body: JSON.stringify({
          title: formData.title,
          description: formData.description,
          category: formData.category,
          tags: formData.tags.split(',').map(t => t.trim()).filter(Boolean),
        }),
      });
      if (res.ok) {
        setShowCreate(false);
        await fetchCauses();
        addToast('Cause created', 'success');
      } else {
        addToast('Failed to create cause', 'error');
      }
    } catch (err) {
      console.error('Create cause error:', err);
      addToast('Failed to create cause', 'error');
    }
  };

  const filtered = causes.filter(c =>
    !debouncedSearch || c.title.toLowerCase().includes(debouncedSearch.toLowerCase()) ||
    (c.description && c.description.toLowerCase().includes(debouncedSearch.toLowerCase()))
  );

  return (
    <div className="p-6 space-y-6">
      {/* Header */}
      <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)' }}>
            <Flame size={20} style={{ color: '#ef4444' }} />
            Causes
          </h2>
          <p className="text-sm mt-1" style={{ color: 'var(--color-text-muted)' }}>
            Browse and vote on causes that matter most
          </p>
        </div>
        <div className="flex items-center gap-2">
          <button
            onClick={handleDiscover}
            disabled={discovering}
            className="btn btn-primary"
          >
            {discovering ? <Loader2 size={14} className="animate-spin" /> : <Sparkles size={14} />}
            {discovering ? 'Discovering...' : 'AI Discover Causes'}
          </button>
          <button onClick={() => setShowCreate(true)} className="btn btn-secondary">
            <Plus size={14} />
            Add Cause
          </button>
        </div>
      </div>

      {/* Filters */}
      <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 causes..."
            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="urgency">Sort by Urgency</option>
          <option value="votes">Sort by Votes</option>
          <option value="trending">Sort by Trending</option>
        </select>
      </div>

      {/* Category Pills */}
      <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>

      {/* Causes Grid */}
      {loading ? (
        <SkeletonList count={4} />
      ) : (
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
          <AnimatePresence>
            {filtered.map((cause, i) => (
              <motion.div
                key={cause.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)' }}>
                      {cause.title}
                    </h3>
                    {cause.category && (
                      <span className="badge badge-amber text-xs mt-1">
                        {cause.category.replace('_', ' ')}
                      </span>
                    )}
                  </div>
                  <div className="flex items-center gap-1">
                    {cause.source === 'ai' && (
                      <span title="AI Discovered"><Sparkles size={12} style={{ color: '#c084fc' }} /></span>
                    )}
                    {cause.urgency_score > 0.7 && (
                      <ArrowUpRight size={14} style={{ color: '#ef4444' }} />
                    )}
                  </div>
                </div>

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

                {/* Urgency Gauge */}
                <div>
                  <div className="flex items-center justify-between text-xs mb-1">
                    <span style={{ color: 'var(--color-text-muted)' }}>Urgency</span>
                    <span style={{
                      color: cause.urgency_score > 0.7 ? '#ef4444' : cause.urgency_score > 0.4 ? '#d97706' : '#22c55e',
                      fontWeight: 600,
                    }}>
                      {(cause.urgency_score * 100).toFixed(0)}%
                    </span>
                  </div>
                  <div className="urgency-gauge">
                    <div
                      className="urgency-gauge-fill"
                      style={{
                        width: `${cause.urgency_score * 100}%`,
                        background: cause.urgency_score > 0.7
                          ? 'linear-gradient(90deg, #d97706, #ef4444)'
                          : cause.urgency_score > 0.4
                            ? 'linear-gradient(90deg, #22c55e, #d97706)'
                            : '#22c55e',
                      }}
                    />
                  </div>
                </div>

                {/* Stats */}
                <div className="flex items-center gap-3 text-xs" style={{ color: 'var(--color-text-muted)' }}>
                  <span className="flex items-center gap-1">
                    <Users size={12} /> {cause.total_votes} votes
                  </span>
                  <span className="flex items-center gap-1">
                    <TrendingUp size={12} /> {cause.total_supporters} supporters
                  </span>
                </div>

                {/* Tags */}
                {cause.tags && cause.tags.length > 0 && (
                  <div className="flex items-center gap-1 flex-wrap">
                    {cause.tags.slice(0, 4).map((tag: string) => (
                      <span key={tag} 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)',
                        }}
                      >
                        <Tag size={10} style={{ display: 'inline', marginRight: 2 }} />
                        {tag}
                      </span>
                    ))}
                  </div>
                )}
              </motion.div>
            ))}
          </AnimatePresence>
        </div>
      )}

      {!loading && filtered.length === 0 && (
        <div className="text-center py-12">
          <Flame size={32} style={{ color: 'var(--color-text-muted)', margin: '0 auto 12px' }} />
          <p style={{ color: 'var(--color-text-muted)' }}>
            No causes found. Click "AI Discover Causes" to find trending issues.
          </p>
        </div>
      )}

      {/* Create Cause Modal */}
      {showCreate && (
        <CreateCauseModal onClose={() => setShowCreate(false)} onSubmit={handleCreateCause} />
      )}
    </div>
  );
}

function CreateCauseModal({
  onClose,
  onSubmit,
}: {
  onClose: () => void;
  onSubmit: (data: { title: string; description: string; category: string; tags: string }) => void;
}) {
  const [title, setTitle] = useState('');
  const [description, setDescription] = useState('');
  const [cat, setCat] = useState('other');
  const [tags, setTags] = useState('');

  useEffect(() => {
    const handleKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
    window.addEventListener('keydown', handleKey);
    return () => window.removeEventListener('keydown', handleKey);
  }, [onClose]);

  return (
    <div
      className="fixed inset-0 z-50 flex items-center justify-center p-4"
      style={{ backgroundColor: 'rgba(0,0,0,0.6)' }}
      onClick={onClose}
    >
      <motion.div
        initial={{ opacity: 0, scale: 0.95 }}
        animate={{ opacity: 1, scale: 1 }}
        className="w-full max-w-md rounded-xl p-6"
        style={{
          backgroundColor: 'var(--color-surface)',
          border: '1px solid var(--color-border)',
        }}
        onClick={(e) => e.stopPropagation()}
      >
        <div className="flex items-center justify-between mb-4">
          <h3 className="text-lg font-semibold" style={{ color: 'var(--color-text)' }}>Add New Cause</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)' }}>Title</label>
            <input className="input" value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Clean Water Access" />
          </div>
          <div>
            <label className="block text-sm mb-1" style={{ color: 'var(--color-text-secondary)' }}>Description</label>
            <textarea className="input" rows={3} value={description} onChange={(e) => setDescription(e.target.value)} placeholder="Why this cause matters..." />
          </div>
          <div>
            <label className="block text-sm mb-1" style={{ color: 'var(--color-text-secondary)' }}>Category</label>
            <select className="input" value={cat} onChange={(e) => setCat(e.target.value)}>
              {CATEGORIES.filter(c => c !== 'all').map(c => (
                <option key={c} value={c}>{c.replace('_', ' ')}</option>
              ))}
            </select>
          </div>
          <div>
            <label className="block text-sm mb-1" style={{ color: 'var(--color-text-secondary)' }}>Tags (comma-separated)</label>
            <input className="input" value={tags} onChange={(e) => setTags(e.target.value)} placeholder="water, health, access" />
          </div>
          <div className="flex justify-end gap-2 pt-2">
            <button onClick={onClose} className="btn btn-secondary">Cancel</button>
            <button
              onClick={() => onSubmit({ title, description, category: cat, tags })}
              disabled={!title}
              className="btn btn-primary"
            >
              Create Cause
            </button>
          </div>
        </div>
      </motion.div>
    </div>
  );
}