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