← back to Patty
app/pulse/PulseTable.tsx
114 lines
/**
* PulseTable — server-component sortable list of the highest-velocity
* petitions, paired with PulseCanvas. Static table (no interactive sort) so
* the page stays light and the canvas component is the only client island.
*
* Mirrors Grant's RiverTable role — the visualization is the hero, the table
* is the "give me the actual numbers" companion.
*/
import type { Petition } from './PulseCanvas';
const CATEGORY_COLOR: Record<Petition['category'], string> = {
climate: '#10b981',
ai_safety: '#a78bfa',
governance: '#fbbf24',
civil_rights: '#fb7185',
economic_justice: '#38bdf8',
healthcare: '#ec4899',
};
const CATEGORY_LABEL: Record<Petition['category'], string> = {
climate: 'Climate',
ai_safety: 'AI Safety',
governance: 'Governance',
civil_rights: 'Civil Rights',
economic_justice: 'Economic Justice',
healthcare: 'Healthcare',
};
export default function PulseTable({ petitions }: { petitions: Petition[] }) {
// Sort by velocity descending, top 10 shown.
const top = [...petitions].sort((a, b) => b.velocity - a.velocity).slice(0, 10);
return (
<div style={{
border: '1px solid #1a1326', borderRadius: 8, overflow: 'hidden',
background: '#0d0a18',
}}>
<table style={{
width: '100%', borderCollapse: 'collapse', fontSize: 12, color: '#c8b8d8',
}}>
<thead>
<tr style={{ background: '#100b1f', color: '#7a6c8a',
fontSize: 10, letterSpacing: '0.08em', textTransform: 'uppercase' }}>
<th style={{ textAlign: 'left', padding: '10px 16px', fontWeight: 500 }}>Petition</th>
<th style={{ textAlign: 'left', padding: '10px 16px', fontWeight: 500 }}>Category</th>
<th style={{ textAlign: 'right', padding: '10px 16px', fontWeight: 500 }}>Velocity</th>
<th style={{ textAlign: 'right', padding: '10px 16px', fontWeight: 500 }}>Signatures</th>
<th style={{ textAlign: 'right', padding: '10px 16px', fontWeight: 500 }}>Progress</th>
<th style={{ textAlign: 'right', padding: '10px 16px', fontWeight: 500 }}>Age</th>
</tr>
</thead>
<tbody>
{top.map((p, i) => {
const pct = Math.round(100 * p.signatures / Math.max(1, p.goal));
const color = CATEGORY_COLOR[p.category];
return (
<tr key={p.id} style={{
borderTop: i === 0 ? 'none' : '1px solid #1a1326',
}}>
<td style={{ padding: '12px 16px', lineHeight: 1.4 }}>
<a href={`/petitions/${p.slug}`}
style={{ color: '#e4d8f1', textDecoration: 'none' }}>
{p.title}
</a>
</td>
<td style={{ padding: '12px 16px' }}>
<span style={{
fontSize: 10, padding: '2px 8px', borderRadius: 99,
background: `${color}1a`, color, letterSpacing: '0.03em',
}}>
{CATEGORY_LABEL[p.category]}
</span>
</td>
<td style={{ padding: '12px 16px', textAlign: 'right',
fontVariantNumeric: 'tabular-nums', color, fontWeight: 500 }}>
{p.velocity.toLocaleString()}/d
</td>
<td style={{ padding: '12px 16px', textAlign: 'right',
fontVariantNumeric: 'tabular-nums' }}>
{p.signatures.toLocaleString()}
<span style={{ color: '#5a4c70', fontSize: 11 }}> / {p.goal.toLocaleString()}</span>
</td>
<td style={{ padding: '12px 16px', textAlign: 'right',
fontVariantNumeric: 'tabular-nums' }}>
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
<div style={{
width: 60, height: 4, background: '#1a1326',
borderRadius: 2, overflow: 'hidden',
}}>
<div style={{
width: `${Math.min(100, pct)}%`, height: '100%',
background: color, opacity: 0.85,
}} />
</div>
<span style={{ color: pct >= 75 ? color : '#c8b8d8',
minWidth: 32, textAlign: 'right' }}>
{pct}%
</span>
</div>
</td>
<td style={{ padding: '12px 16px', textAlign: 'right',
color: '#7a6c8a', fontVariantNumeric: 'tabular-nums' }}>
{p.daysSinceStart}d
</td>
</tr>
);
})}
</tbody>
</table>
</div>
);
}