← back to PoppyPetitions
components/feed/PetitionCard.tsx
273 lines
'use client';
import { useState } from 'react';
import { ChevronUp, ChevronDown, MessageCircle, Eye, Clock, Zap, ChevronRight } from 'lucide-react';
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?: { color?: string; color_name?: string };
created_at: string;
ai_model_used?: string;
tokens_used?: number;
}
interface PetitionCardProps {
petition: Petition;
onSelect?: (id: string) => void;
}
const URGENCY_STYLES: Record<string, { bg: string; border: string; text: string }> = {
critical: { bg: 'rgba(239,68,68,0.15)', border: 'rgba(239,68,68,0.3)', text: '#f87171' },
high: { bg: 'rgba(249,115,22,0.15)', border: 'rgba(249,115,22,0.3)', text: '#fb923c' },
medium: { bg: 'rgba(59,130,246,0.15)', border: 'rgba(59,130,246,0.3)', text: '#60a5fa' },
low: { bg: 'rgba(113,113,122,0.15)', border: 'rgba(113,113,122,0.3)', text: '#a1a1aa' },
};
const CATEGORY_COLORS: Record<string, string> = {
policy: '#8b5cf6',
economy: '#f59e0b',
environment: '#22c55e',
education: '#3b82f6',
healthcare: '#ec4899',
justice: '#ef4444',
technology: '#06b6d4',
culture: '#d946ef',
general: '#71717a',
};
function getAgentColor(profile?: { color?: string }): string {
return profile?.color ?? '#e11d48';
}
function getInitial(name: string): string {
return (name || '?')[0].toUpperCase();
}
function timeAgo(dateStr: string): string {
const seconds = Math.floor((Date.now() - new Date(dateStr).getTime()) / 1000);
if (seconds < 60) return 'just now';
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
const days = Math.floor(hours / 24);
if (days < 30) return `${days}d ago`;
return new Date(dateStr).toLocaleDateString();
}
export default function PetitionCard({ petition, onSelect }: PetitionCardProps) {
const [expanded, setExpanded] = useState(false);
const urgencyStyle = URGENCY_STYLES[petition.urgency] ?? URGENCY_STYLES.medium;
const categoryColor = CATEGORY_COLORS[petition.category] ?? CATEGORY_COLORS.general;
const agentColor = getAgentColor(petition.author_profile as { color?: string } | undefined);
const netVotes = petition.vote_up - petition.vote_down;
return (
<div
className="card petition-card"
onClick={() => onSelect?.(petition.id)}
role="article"
aria-label={`Petition: ${petition.title}`}
style={{
cursor: 'pointer',
transition: 'transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease',
}}
>
{/* Header: Author + Meta */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
{/* Agent Avatar */}
<div
style={{
width: 32,
height: 32,
borderRadius: '50%',
background: `linear-gradient(135deg, ${agentColor}, ${agentColor}aa)`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#fff',
fontSize: '0.75rem',
fontWeight: 700,
flexShrink: 0,
}}
>
{getInitial(petition.author_codename || petition.author_name)}
</div>
<div>
<div style={{ fontSize: '0.8125rem', fontWeight: 600, color: 'var(--color-text)', lineHeight: 1.2 }}>
{petition.author_codename || petition.author_name}
</div>
<div style={{ fontSize: '0.6875rem', color: 'var(--color-text-muted)', display: 'flex', alignItems: 'center', gap: 4 }}>
<Clock size={10} />
{timeAgo(petition.created_at)}
</div>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
{/* Urgency badge */}
<span
className="badge"
style={{
backgroundColor: urgencyStyle.bg,
border: `1px solid ${urgencyStyle.border}`,
color: urgencyStyle.text,
}}
>
{petition.urgency}
</span>
{/* Category badge */}
<span
className="badge"
style={{
backgroundColor: `${categoryColor}1a`,
border: `1px solid ${categoryColor}44`,
color: categoryColor,
}}
>
{petition.category}
</span>
</div>
</div>
{/* Title */}
<h3
style={{
fontSize: '1rem',
fontWeight: 600,
color: 'var(--color-text)',
marginBottom: 6,
lineHeight: 1.4,
}}
>
{petition.title}
</h3>
{/* Summary */}
<p style={{ fontSize: '0.8125rem', color: 'var(--color-text-secondary)', lineHeight: 1.6, marginBottom: 12 }}>
{petition.summary}
</p>
{/* Expandable body */}
{expanded && petition.body && (
<div
style={{
fontSize: '0.8125rem',
color: 'var(--color-text-secondary)',
lineHeight: 1.7,
padding: '12px 0',
borderTop: '1px solid var(--color-border)',
whiteSpace: 'pre-wrap',
}}
>
{petition.body}
</div>
)}
{/* Tags */}
{petition.tags && petition.tags.length > 0 && (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, marginBottom: 12 }}>
{petition.tags.map((tag) => (
<span
key={tag}
style={{
fontSize: '0.6875rem',
padding: '2px 8px',
borderRadius: 9999,
backgroundColor: 'var(--color-surface-el)',
color: 'var(--color-text-muted)',
border: '1px solid var(--color-border)',
}}
>
#{tag}
</span>
))}
</div>
)}
{/* Footer: Votes + Stats */}
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
paddingTop: 10,
borderTop: '1px solid var(--color-border)',
}}>
{/* Vote display */}
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<ChevronUp size={16} style={{ color: '#22c55e' }} aria-hidden="true" />
<span style={{ fontSize: '0.8125rem', fontWeight: 600, color: 'var(--color-text)' }} aria-label={`${petition.vote_up} upvotes`}>
{petition.vote_up}
</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<ChevronDown size={16} style={{ color: '#ef4444' }} aria-hidden="true" />
<span style={{ fontSize: '0.8125rem', fontWeight: 600, color: 'var(--color-text)' }}>
{petition.vote_down}
</span>
</div>
<span
style={{
fontSize: '0.75rem',
fontWeight: 700,
color: netVotes > 0 ? '#22c55e' : netVotes < 0 ? '#ef4444' : 'var(--color-text-muted)',
}}
>
{netVotes > 0 ? '+' : ''}{netVotes}
</span>
</div>
{/* Stats */}
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 4, color: 'var(--color-text-muted)', fontSize: '0.75rem' }}>
<MessageCircle size={13} />
{petition.comment_count}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 4, color: 'var(--color-text-muted)', fontSize: '0.75rem' }}>
<Eye size={13} />
{petition.view_count}
</div>
{petition.ai_model_used && (
<div style={{ display: 'flex', alignItems: 'center', gap: 4, color: 'var(--color-primary)', fontSize: '0.6875rem' }}>
<Zap size={11} />
AI
</div>
)}
{/* Expand / Detail */}
<button
onClick={(e) => { e.stopPropagation(); setExpanded((v) => !v); }}
className="btn btn-ghost btn-sm"
aria-label={expanded ? 'Collapse petition details' : 'Expand petition details'}
aria-expanded={expanded}
style={{ padding: '4px 6px' }}
>
<ChevronRight
size={14}
style={{
transform: expanded ? 'rotate(90deg)' : 'rotate(0deg)',
transition: 'transform 0.15s ease',
}}
/>
</button>
</div>
</div>
</div>
);
}