← 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>
);
}