← back to Patty

components/trends/TrendsTab.tsx

535 lines

'use client';

import { useState, useEffect, useCallback } from 'react';
import { SkeletonList } from '../Skeleton';
import {
  TrendingUp, Sparkles, RefreshCw, Loader2, Clock,
  Megaphone, X, Plus, Target,
} from 'lucide-react';
import { useToast } from '../ToastProvider';
import SortDropdown from '../shared/SortDropdown';
import { useClientSort, SortConfig } from '@/hooks/useClientSort';

const TREND_SORT_OPTIONS = [
  { value: 'engagement_desc', label: 'Most Engaging' },
  { value: 'date_desc', label: 'Newest First' },
  { value: 'date_asc', label: 'Oldest First' },
  { value: 'sentiment', label: 'Sentiment A-Z' },
];

const TREND_SORT_CONFIGS: Record<string, SortConfig> = {
  engagement_desc: { key: 'engagement_score', direction: 'desc', type: 'number' },
  date_desc: { key: 'created_at', direction: 'desc', type: 'date' },
  date_asc: { key: 'created_at', direction: 'asc', type: 'date' },
  sentiment: { key: 'sentiment', direction: 'asc', type: 'string' },
};

/* ─── Types ─────────────────────────────────────────────────── */
interface TrendingTopic {
  id: string;
  source: string;
  source_url: string | null;
  title: string;
  content: string | null;
  engagement_score: number | null;
  sentiment: string | null;
  category: string | null;
  tags: string[] | null;
  is_used: boolean;
  expires_at: string | null;
  created_at: string;
}

/* ─── Component ─────────────────────────────────────────────── */
export default function TrendsTab() {
  const { addToast } = useToast();
  const [topics, setTopics] = useState<TrendingTopic[]>([]);
  const [loading, setLoading] = useState(true);
  const [discovering, setDiscovering] = useState(false);
  const [categoryFilter, setCategoryFilter] = useState('all');
  const [sortBy, setSortBy] = useState('engagement_desc');
  const sortedTopics = useClientSort(topics, sortBy, TREND_SORT_CONFIGS);

  // Petition creation modal (pre-filled from trend)
  const [createFromTrend, setCreateFromTrend] = useState<TrendingTopic | null>(null);
  const [petTopic, setPetTopic] = useState('');
  const [petTarget, setPetTarget] = useState('');
  const [petTone, setPetTone] = useState('urgent');
  const [petCategory, setPetCategory] = useState('policy');
  const [generating, setGenerating] = useState(false);
  const [saving, setSaving] = useState(false);
  const [generated, setGenerated] = useState<{
    title: string; summary: string; body_html: string; body_text: string;
    suggested_target: string; suggested_tags: string[]; suggested_goal: number;
  } | null>(null);

  /* ── Fetch ──────────────────────────────────────────────────── */
  const fetchTopics = useCallback(async () => {
    setLoading(true);
    try {
      const params = new URLSearchParams();
      if (categoryFilter !== 'all') params.set('category', categoryFilter);
      const res = await fetch(`/api/trending?${params.toString()}`);
      if (res.ok) {
        const data = await res.json();
        setTopics(data.rows || []);
      }
    } catch (err) {
      console.error('Fetch trends error:', err);
    } finally {
      setLoading(false);
    }
  }, [categoryFilter]);

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

  /* ── Escape key handler ───────────────────────────────────────── */
  useEffect(() => {
    if (!createFromTrend) return;
    const handleKey = (e: KeyboardEvent) => { if (e.key === 'Escape') setCreateFromTrend(null); };
    window.addEventListener('keydown', handleKey);
    return () => window.removeEventListener('keydown', handleKey);
  }, [createFromTrend]);

  /* ── AI Discover ────────────────────────────────────────────── */
  async function handleDiscover() {
    setDiscovering(true);
    try {
      const res = await fetch('/api/trending/discover', { method: 'POST' });
      if (res.ok) {
        fetchTopics();
        addToast('Trends discovered', 'success');
      } else {
        addToast('Failed to discover trends', 'error');
      }
    } catch (err) {
      console.error('Discover error:', err);
      addToast('Failed to discover trends', 'error');
    } finally {
      setDiscovering(false);
    }
  }

  /* ── Create Petition from Trend ─────────────────────────────── */
  function openCreateFromTrend(topic: TrendingTopic) {
    setCreateFromTrend(topic);
    setPetTopic(topic.title);
    setPetCategory(topic.category || 'policy');
    setPetTarget('');
    setPetTone('urgent');
    setGenerated(null);
  }

  async function handleGenerate() {
    setGenerating(true);
    setGenerated(null);
    try {
      const res = await fetch('/api/petitions/generate', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ topic: petTopic, target: petTarget, tone: petTone, category: petCategory }),
      });
      if (res.ok) {
        const data = await res.json();
        setGenerated(data);
        addToast('Petition generated', 'success');
      } else {
        addToast('Generation failed', 'error');
      }
    } catch (err) {
      console.error('Generate error:', err);
      addToast('Generation failed', 'error');
    } finally {
      setGenerating(false);
    }
  }

  async function handleSavePetition() {
    if (!generated) return;
    setSaving(true);
    try {
      const res = await fetch('/api/petitions', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          title: generated.title,
          summary: generated.summary,
          body_html: generated.body_html,
          body_text: generated.body_text,
          target: generated.suggested_target,
          category: petCategory,
          tags: generated.suggested_tags,
          signature_goal: generated.suggested_goal,
          status: 'draft',
        }),
      });
      if (res.ok) {
        setCreateFromTrend(null);
        setGenerated(null);
        addToast('Petition saved as draft', 'success');
      } else {
        addToast('Failed to save petition', 'error');
      }
    } catch (err) {
      console.error('Save petition error:', err);
      addToast('Failed to save petition', 'error');
    } finally {
      setSaving(false);
    }
  }

  /* ── Helpers ────────────────────────────────────────────────── */
  const categories = ['all', 'debt_relief', 'policy', 'education_access', 'consumer_protection', 'federal_budget'];

  const categoryLabels: Record<string, string> = {
    all: 'All',
    debt_relief: 'Debt Relief',
    policy: 'Policy',
    education_access: 'Education Access',
    consumer_protection: 'Consumer Protection',
    federal_budget: 'Federal Budget',
  };

  const sentimentColors: Record<string, { bg: string; text: string; border: string }> = {
    positive: { bg: 'rgba(34,197,94,0.15)', text: 'var(--color-success)', border: 'rgba(34,197,94,0.3)' },
    negative: { bg: 'rgba(239,68,68,0.15)', text: 'var(--color-error)', border: 'rgba(239,68,68,0.3)' },
    neutral: { bg: 'rgba(161,161,170,0.15)', text: 'var(--color-text-secondary)', border: 'rgba(161,161,170,0.3)' },
    mixed: { bg: 'rgba(245,158,11,0.15)', text: 'var(--color-warning)', border: 'rgba(245,158,11,0.3)' },
  };

  const sourceColors: Record<string, string> = {
    'Twitter/X': '#1DA1F2',
    'Reddit': '#FF4500',
    'News': '#22c55e',
    'Congress.gov': '#3b82f6',
    'Change.org': '#E84444',
    'TikTok': '#ff2d55',
    'Instagram': '#C13584',
  };

  const expiresIn = (d: string | null) => {
    if (!d) return null;
    const diff = new Date(d).getTime() - Date.now();
    if (diff <= 0) return 'Expired';
    const days = Math.floor(diff / (1000 * 60 * 60 * 24));
    const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
    if (days > 0) return `${days}d ${hours}h left`;
    return `${hours}h left`;
  };

  function stripHtml(html: string): string {
    return html.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
  }

  /* ─── Render ────────────────────────────────────────────────── */
  return (
    <div style={{ padding: '24px' }}>
      {/* Header */}
      <div className="flex items-center justify-between mb-6">
        <div>
          <h2 className="text-lg font-semibold" style={{ color: 'var(--color-text)' }}>
            Trending Topics
          </h2>
          <p className="text-sm mt-1" style={{ color: 'var(--color-text-muted)' }}>
            AI-curated topics ready for petition creation
          </p>
        </div>
        <button
          className="btn btn-primary"
          onClick={handleDiscover}
          disabled={discovering}
        >
          {discovering ? (
            <><Loader2 size={16} className="animate-spin" /> Discovering...</>
          ) : (
            <><Sparkles size={16} /> AI Discover Trends</>
          )}
        </button>
      </div>

      {/* Sort + Category Filter */}
      <div className="flex items-center gap-2 mb-6 flex-wrap">
        <SortDropdown options={TREND_SORT_OPTIONS} value={sortBy} onChange={setSortBy} />
        {categories.map((c) => (
          <button
            key={c}
            onClick={() => setCategoryFilter(c)}
            className={`btn btn-sm ${categoryFilter === c ? 'btn-primary' : 'btn-ghost'}`}
          >
            {categoryLabels[c] || c}
          </button>
        ))}
      </div>

      {/* Loading */}
      {loading && (
        <SkeletonList count={4} />
      )}

      {/* Empty State */}
      {!loading && topics.length === 0 && (
        <div className="card flex flex-col items-center justify-center py-16" style={{ textAlign: 'center' }}>
          <div
            className="w-16 h-16 rounded-2xl flex items-center justify-center mb-4"
            style={{
              background: 'linear-gradient(135deg, rgba(124, 58, 237, 0.15), rgba(167, 139, 250, 0.1))',
              border: '1px solid rgba(124, 58, 237, 0.2)',
            }}
          >
            <TrendingUp size={28} style={{ color: 'var(--color-primary)' }} />
          </div>
          <h3 className="text-base font-semibold mb-2" style={{ color: 'var(--color-text)' }}>
            No trending topics yet
          </h3>
          <p className="text-sm mb-6 max-w-md" style={{ color: 'var(--color-text-muted)' }}>
            Use AI to discover trending topics about student debt and education policy.
          </p>
          <button className="btn btn-primary" onClick={handleDiscover} disabled={discovering}>
            {discovering ? (
              <><Loader2 size={16} className="animate-spin" /> Discovering...</>
            ) : (
              <><Sparkles size={16} /> Discover Trends Now</>
            )}
          </button>

          {/* How It Works */}
          <div className="mt-10 grid grid-cols-1 sm:grid-cols-3 gap-4 w-full max-w-2xl">
            {[
              { title: 'Discover', desc: 'AI scans news, social media, and gov sites for trending civic issues' },
              { title: 'Analyze', desc: 'Each topic is scored for engagement potential and sentiment' },
              { title: 'Create', desc: 'One click to generate a petition draft from any trending topic' },
            ].map((step, i) => (
              <div key={i} className="card-elevated" style={{ padding: '16px', textAlign: 'left' }}>
                <div className="text-xs font-bold mb-2" style={{ color: 'var(--color-primary)' }}>
                  Step {i + 1}
                </div>
                <div className="text-sm font-semibold mb-1" style={{ color: 'var(--color-text)' }}>
                  {step.title}
                </div>
                <div className="text-xs" style={{ color: 'var(--color-text-muted)' }}>
                  {step.desc}
                </div>
              </div>
            ))}
          </div>
        </div>
      )}

      {/* Topic Cards */}
      {!loading && sortedTopics.length > 0 && (
        <div className="flex flex-col gap-3">
          {sortedTopics.map((t) => {
            const sent = sentimentColors[t.sentiment || 'neutral'] || sentimentColors.neutral;
            const srcColor = sourceColors[t.source] || 'var(--color-text-muted)';
            const expiry = expiresIn(t.expires_at);
            const score = t.engagement_score || 0;
            return (
              <div key={t.id} className="card">
                <div className="flex items-start gap-4">
                  <div className="flex-1 min-w-0">
                    {/* Title + Badges */}
                    <div className="flex items-center gap-2 mb-2 flex-wrap">
                      <h3 className="text-sm font-semibold" style={{ color: 'var(--color-text)' }}>
                        {t.title}
                      </h3>
                      {/* Source badge */}
                      <span
                        className="badge"
                        style={{
                          backgroundColor: `${srcColor}20`,
                          color: srcColor,
                          border: `1px solid ${srcColor}40`,
                        }}
                      >
                        {t.source}
                      </span>
                      {/* Sentiment badge */}
                      <span
                        className="badge"
                        style={{
                          backgroundColor: sent.bg,
                          color: sent.text,
                          border: `1px solid ${sent.border}`,
                        }}
                      >
                        {t.sentiment || 'neutral'}
                      </span>
                      {/* Category */}
                      {t.category && (
                        <span
                          className="badge"
                          style={{
                            backgroundColor: 'rgba(124, 58, 237, 0.1)',
                            color: 'var(--color-secondary)',
                            border: '1px solid rgba(124, 58, 237, 0.2)',
                          }}
                        >
                          {categoryLabels[t.category] || t.category}
                        </span>
                      )}
                    </div>

                    {/* Content */}
                    {t.content && (
                      <p className="text-xs mb-3 line-clamp-2" style={{ color: 'var(--color-text-muted)' }}>
                        {t.content}
                      </p>
                    )}

                    {/* Engagement Score Bar */}
                    <div className="flex items-center gap-3 mb-2">
                      <span className="text-xs" style={{ color: 'var(--color-text-muted)', width: 80 }}>
                        Engagement
                      </span>
                      <div
                        style={{
                          flex: 1, maxWidth: 200, height: 6, borderRadius: 3,
                          backgroundColor: 'var(--color-surface-el)', overflow: 'hidden',
                        }}
                      >
                        <div
                          style={{
                            width: `${score}%`, height: '100%', borderRadius: 3,
                            background: score > 70 ? 'linear-gradient(90deg, #22c55e, #4ade80)' :
                              score > 40 ? 'linear-gradient(90deg, #f59e0b, #fbbf24)' :
                                'linear-gradient(90deg, #ef4444, #f87171)',
                          }}
                        />
                      </div>
                      <span className="text-xs font-semibold" style={{ color: 'var(--color-text-secondary)', width: 30 }}>
                        {score}
                      </span>
                    </div>

                    {/* Tags + Expiry */}
                    <div className="flex items-center gap-2 flex-wrap">
                      {t.tags && t.tags.map((tag, i) => (
                        <span key={i} className="text-xs" style={{
                          color: 'var(--color-secondary)',
                          padding: '1px 6px', borderRadius: 9999,
                          backgroundColor: 'rgba(124, 58, 237, 0.08)',
                        }}>
                          {tag}
                        </span>
                      ))}
                      {expiry && (
                        <span className="flex items-center gap-1 text-xs" style={{ color: 'var(--color-text-muted)', marginLeft: 'auto' }}>
                          <Clock size={11} /> {expiry}
                        </span>
                      )}
                    </div>
                  </div>

                  {/* Action */}
                  <button
                    className="btn btn-sm btn-primary shrink-0"
                    onClick={() => openCreateFromTrend(t)}
                  >
                    <Megaphone size={14} />
                    Create Petition
                  </button>
                </div>
              </div>
            );
          })}
        </div>
      )}

      {/* ─── Create Petition from Trend Modal ────────────────────── */}
      {createFromTrend && (
        <div
          style={{
            position: 'fixed', inset: 0, zIndex: 50,
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            backgroundColor: 'rgba(0,0,0,0.6)',
            backdropFilter: 'blur(4px)',
          }}
          onClick={(e) => { if (e.target === e.currentTarget) setCreateFromTrend(null); }}
        >
          <div
            style={{
              width: '100%', maxWidth: 600, maxHeight: '90vh', overflow: 'auto',
              backgroundColor: 'var(--color-surface)',
              border: '1px solid var(--color-border)',
              borderRadius: 12, padding: 24,
            }}
          >
            <div className="flex items-center justify-between mb-4">
              <h3 className="text-base font-semibold" style={{ color: 'var(--color-text)' }}>
                Create Petition from Trend
              </h3>
              <button className="btn btn-ghost btn-sm" onClick={() => setCreateFromTrend(null)}>
                <X size={16} />
              </button>
            </div>

            {!generated ? (
              <div className="flex flex-col gap-4">
                <div>
                  <label className="block text-xs font-medium mb-1" style={{ color: 'var(--color-text-secondary)' }}>Topic</label>
                  <input className="input" value={petTopic} onChange={(e) => setPetTopic(e.target.value)} />
                </div>
                <div>
                  <label className="block text-xs font-medium mb-1" style={{ color: 'var(--color-text-secondary)' }}>Target</label>
                  <input className="input" placeholder="e.g., Congress" value={petTarget} onChange={(e) => setPetTarget(e.target.value)} />
                </div>
                <div className="grid grid-cols-2 gap-4">
                  <div>
                    <label className="block text-xs font-medium mb-1" style={{ color: 'var(--color-text-secondary)' }}>Tone</label>
                    <select className="input" value={petTone} onChange={(e) => setPetTone(e.target.value)}>
                      <option value="urgent">Urgent</option>
                      <option value="formal">Formal</option>
                      <option value="passionate">Passionate</option>
                      <option value="grassroots">Grassroots</option>
                    </select>
                  </div>
                  <div>
                    <label className="block text-xs font-medium mb-1" style={{ color: 'var(--color-text-secondary)' }}>Category</label>
                    <select className="input" value={petCategory} onChange={(e) => setPetCategory(e.target.value)}>
                      <option value="debt_relief">Debt Relief</option>
                      <option value="policy">Policy</option>
                      <option value="education_access">Education Access</option>
                      <option value="consumer_protection">Consumer Protection</option>
                      <option value="federal_budget">Federal Budget</option>
                    </select>
                  </div>
                </div>
                <button className="btn btn-primary w-full" onClick={handleGenerate} disabled={generating}>
                  {generating ? <><Loader2 size={16} className="animate-spin" /> Generating...</> : <><Sparkles size={16} /> Generate Petition</>}
                </button>
              </div>
            ) : (
              <div className="flex flex-col gap-4">
                <div>
                  <div className="text-xs font-medium mb-1" style={{ color: 'var(--color-text-muted)' }}>Title</div>
                  <div className="text-sm font-semibold" style={{ color: 'var(--color-text)' }}>{generated.title}</div>
                </div>
                <div>
                  <div className="text-xs font-medium mb-1" style={{ color: 'var(--color-text-muted)' }}>Summary</div>
                  <div className="text-sm" style={{ color: 'var(--color-text-secondary)' }}>{generated.summary}</div>
                </div>
                <div>
                  <div className="text-xs font-medium mb-1" style={{ color: 'var(--color-text-muted)' }}>Body Preview</div>
                  <div className="text-xs" style={{
                    color: 'var(--color-text-muted)', maxHeight: 160, overflow: 'auto',
                    padding: 8, borderRadius: 6, backgroundColor: 'var(--color-surface-el)',
                    whiteSpace: 'pre-wrap',
                  }}>
                    {stripHtml(generated.body_html)}
                  </div>
                </div>
                <div className="flex items-center gap-3">
                  <button className="btn btn-primary flex-1" onClick={handleSavePetition} disabled={saving}>
                    {saving ? <><Loader2 size={16} className="animate-spin" /> Saving...</> : <><Plus size={16} /> Save as Draft</>}
                  </button>
                  <button className="btn btn-ghost" onClick={() => setGenerated(null)}>Regenerate</button>
                </div>
              </div>
            )}
          </div>
        </div>
      )}
    </div>
  );
}