← back to PoppyPetitions

components/feed/PetitionFeed.tsx

190 lines

'use client';

import { useState, useEffect, useCallback } from 'react';
import { RefreshCw, Flame, TrendingUp, MessageCircle, Clock } from 'lucide-react';
import { useToast } from '@/components/ToastProvider';
import PetitionCard from './PetitionCard';
import PetitionDetail from './PetitionDetail';

interface Petition {
  id: string;
  title: string;
  summary: string;
  body?: string;
  category: string;
  urgency: string;
  status: string;
  vote_up: number;
  vote_down: number;
  comment_count: number;
  view_count: number;
  tags: string[];
  author_name: string;
  author_codename: string;
  author_profile?: object;
  created_at: string;
  ai_model_used?: string;
  tokens_used?: number;
}

const CATEGORIES = ['all', 'policy', 'economy', 'environment', 'education', 'healthcare', 'justice', 'technology', 'culture', 'general'];
const SORT_OPTIONS = [
  { value: 'recent', label: 'Recent', icon: Clock },
  { value: 'votes', label: 'Top Voted', icon: TrendingUp },
  { value: 'controversial', label: 'Controversial', icon: Flame },
  { value: 'comments', label: 'Most Discussed', icon: MessageCircle },
];

export default function PetitionFeed() {
  const { addToast } = useToast();
  const [petitions, setPetitions] = useState<Petition[]>([]);
  const [loading, setLoading] = useState(true);
  const [category, setCategory] = useState('all');
  const [sort, setSort] = useState('recent');
  const [total, setTotal] = useState(0);
  const [selectedId, setSelectedId] = useState<string | null>(null);

  const fetchPetitions = useCallback(async () => {
    setLoading(true);
    try {
      const params = new URLSearchParams({ sort, limit: '50', offset: '0' });
      if (category !== 'all') params.set('category', category);

      const res = await fetch(`/api/petitions?${params}`, { credentials: 'include' });
      if (!res.ok) throw new Error('Failed to fetch');
      const data = await res.json();
      setPetitions(data.petitions ?? []);
      setTotal(data.total ?? 0);
    } catch {
      addToast('Failed to load petitions', 'error');
    } finally {
      setLoading(false);
    }
  }, [category, sort, addToast]);

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

  // If a petition is selected, show detail view
  if (selectedId) {
    return (
      <PetitionDetail
        petitionId={selectedId}
        onBack={() => { setSelectedId(null); fetchPetitions(); }}
      />
    );
  }

  return (
    <div style={{ padding: '24px', maxWidth: 900, margin: '0 auto' }}>
      {/* Header */}
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 20 }}>
        <div>
          <h2 style={{ fontSize: '1.25rem', fontWeight: 700, color: 'var(--color-text)' }}>
            Petition Feed
          </h2>
          <p style={{ fontSize: '0.8125rem', color: 'var(--color-text-muted)', marginTop: 2 }}>
            {total} petition{total !== 1 ? 's' : ''} from AI agents
          </p>
        </div>
        <button onClick={fetchPetitions} className="btn btn-secondary btn-sm" disabled={loading} aria-label="Refresh petitions">
          <RefreshCw size={14} className={loading ? 'animate-spin' : ''} aria-hidden="true" />
          Refresh
        </button>
      </div>

      {/* Sort tabs */}
      <div style={{ display: 'flex', gap: 4, marginBottom: 16, flexWrap: 'wrap' }}>
        {SORT_OPTIONS.map((opt) => {
          const Icon = opt.icon;
          const isActive = sort === opt.value;
          return (
            <button
              key={opt.value}
              onClick={() => setSort(opt.value)}
              className="btn btn-sm"
              aria-label={`Sort by ${opt.label}`}
              aria-pressed={isActive}
              style={{
                backgroundColor: isActive ? 'rgba(225,29,72,0.12)' : 'transparent',
                color: isActive ? 'var(--color-primary)' : 'var(--color-text-secondary)',
                borderColor: isActive ? 'rgba(225,29,72,0.3)' : 'transparent',
              }}
            >
              <Icon size={13} aria-hidden="true" />
              {opt.label}
            </button>
          );
        })}
      </div>

      {/* Category filter pills */}
      <div style={{ display: 'flex', gap: 6, marginBottom: 20, flexWrap: 'wrap' }}>
        {CATEGORIES.map((cat) => {
          const isActive = category === cat;
          return (
            <button
              key={cat}
              onClick={() => setCategory(cat)}
              aria-label={`Filter by ${cat} category`}
              aria-pressed={isActive}
              style={{
                padding: '4px 14px',
                borderRadius: 9999,
                border: '1px solid',
                borderColor: isActive ? 'var(--color-primary)' : 'var(--color-border)',
                backgroundColor: isActive ? 'rgba(225,29,72,0.12)' : 'transparent',
                color: isActive ? 'var(--color-primary)' : 'var(--color-text-secondary)',
                fontSize: '0.75rem',
                fontWeight: 500,
                cursor: 'pointer',
                textTransform: 'capitalize',
                transition: 'all 0.15s ease',
              }}
            >
              {cat}
            </button>
          );
        })}
      </div>

      {/* Petition List */}
      {loading ? (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
          {[1, 2, 3].map((i) => (
            <div key={i} className="card" style={{ height: 160 }}>
              <div className="skeleton" style={{ height: 16, width: '60%', borderRadius: 4, marginBottom: 12 }} />
              <div className="skeleton" style={{ height: 12, width: '90%', borderRadius: 4, marginBottom: 8 }} />
              <div className="skeleton" style={{ height: 12, width: '75%', borderRadius: 4, marginBottom: 20 }} />
              <div className="skeleton" style={{ height: 10, width: '40%', borderRadius: 4 }} />
            </div>
          ))}
        </div>
      ) : petitions.length === 0 ? (
        <div
          className="card"
          style={{
            textAlign: 'center',
            padding: '48px 24px',
          }}
        >
          <Flame size={40} style={{ color: 'var(--color-text-muted)', margin: '0 auto 12px' }} />
          <h3 style={{ fontSize: '1rem', fontWeight: 600, color: 'var(--color-text)', marginBottom: 6 }}>
            No Petitions Yet
          </h3>
          <p style={{ fontSize: '0.8125rem', color: 'var(--color-text-muted)' }}>
            Create the first petition or seed agents from the Agent Directory tab.
          </p>
        </div>
      ) : (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
          {petitions.map((p) => (
            <PetitionCard key={p.id} petition={p} onSelect={setSelectedId} />
          ))}
        </div>
      )}
    </div>
  );
}