← back to Norma

app/pulse/activity/page.tsx

375 lines

'use client';

import React, { useEffect, useState, useCallback } from 'react';
import Link from 'next/link';
import {
  Activity, FileText, Calendar, ChevronLeft, ChevronRight,
  MessageSquare, Sparkles, Target,
} from 'lucide-react';

/* ────────────────────────── Types ────────────────────────── */

interface ActivityItem {
  signature_id: string;
  signed_at: string;
  comment: string | null;
  petition_id: string;
  petition_title: string;
  category: string | null;
  status: string;
  signature_count: number | null;
  signature_goal: number | null;
  is_featured: boolean;
}

/* ────────────────────────── Colors ────────────────────────── */

const C = {
  darkGreen: '#1B4332',
  gold: '#DAA520',
  text: '#1a1a1a',
  textMuted: '#6b7280',
  border: '#e5e7eb',
};

/* ────────────────────────── Helpers ────────────────────────── */

function categoryLabel(cat: string | null): string {
  if (!cat) return 'General';
  return cat.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
}

function categoryColor(cat: string | null): string {
  const colors: Record<string, string> = {
    debt_cancellation: '#dc2626', consumer_protection: '#6366f1',
    voting_rights: '#7c3aed', legal: '#d97706', trending: '#059669',
    education: '#0891b2', environment: '#16a34a', healthcare: '#e11d48',
  };
  return colors[cat || ''] || '#6b7280';
}

function formatDate(dateStr: string): string {
  const d = new Date(dateStr);
  return d.toLocaleDateString('en-US', {
    year: 'numeric', month: 'short', day: 'numeric',
    hour: 'numeric', minute: '2-digit', timeZone: 'America/Los_Angeles',
  });
}

function formatNumber(n: number | null): string {
  if (n === null || n === undefined) return '0';
  if (n >= 1000000) return `${(n / 1000000).toFixed(1)}M`;
  if (n >= 1000) return `${(n / 1000).toFixed(n >= 10000 ? 0 : 1)}K`;
  return n.toString();
}

function progressPercent(count: number | null, goal: number | null): number {
  if (!goal || goal <= 0) return 0;
  return Math.min(100, Math.round(((count || 0) / goal) * 100));
}

/* ────────────────────────── Page ────────────────────────── */

export default function PulseActivityPage() {
  const [activity, setActivity] = useState<ActivityItem[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState('');
  const [page, setPage] = useState(1);
  const [totalPages, setTotalPages] = useState(1);
  const [total, setTotal] = useState(0);

  const fetchActivity = useCallback(async (p: number) => {
    setLoading(true);
    setError('');
    try {
      const res = await fetch(`/api/pulse/activity?page=${p}&limit=20`, { credentials: 'include' });
      if (!res.ok) {
        if (res.status === 401) { setError('Please sign in to view your activity.'); return; }
        throw new Error('Failed to fetch');
      }
      const data = await res.json();
      setActivity(data.activity || []);
      setTotalPages(data.totalPages || 1);
      setTotal(data.total || 0);
    } catch {
      setError('Failed to load activity. Please try again.');
    } finally {
      setLoading(false);
    }
  }, []);

  useEffect(() => { fetchActivity(page); }, [page, fetchActivity]);

  return (
    <div style={{ maxWidth: 900, margin: '0 auto', padding: '40px 24px 80px' }}>
      {/* Header */}
      <div style={{ marginBottom: 32 }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 8 }}>
          <div style={{
            width: 44, height: 44, borderRadius: 12,
            backgroundColor: 'rgba(27,67,50,0.08)',
            display: 'flex', alignItems: 'center', justifyContent: 'center',
          }}>
            <Activity size={22} style={{ color: C.darkGreen }} />
          </div>
          <div>
            <h1 style={{ fontSize: 28, fontWeight: 'bold', color: C.text, margin: 0, fontFamily: 'Georgia, serif' }}>
              My Activity
            </h1>
            <p style={{ fontSize: 14, color: C.textMuted, margin: '4px 0 0', fontFamily: 'system-ui, sans-serif' }}>
              {total > 0 ? `${total} petition${total === 1 ? '' : 's'} signed` : 'Your signature history'}
            </p>
          </div>
        </div>
      </div>

      {/* Error state — for "please sign in" cases, show a CTA button so
          guests have a one-click path to /login instead of a dead red box. */}
      {error && (
        <div style={{
          backgroundColor: /sign in/i.test(error) ? '#f0fdf4' : '#fef2f2',
          border: '1px solid ' + (/sign in/i.test(error) ? '#bbf7d0' : '#fecaca'),
          borderRadius: 12, padding: 24, textAlign: 'center',
          color: /sign in/i.test(error) ? '#14532d' : '#991b1b',
          fontFamily: 'system-ui, sans-serif',
        }}>
          <div style={{ marginBottom: /sign in/i.test(error) ? 12 : 0 }}>{error}</div>
          {/sign in/i.test(error) && (
            <Link
              href="/login"
              style={{
                display: 'inline-block', padding: '10px 24px',
                backgroundColor: C.darkGreen, color: '#fff',
                borderRadius: 8, fontSize: 14, fontWeight: 600,
                textDecoration: 'none',
              }}
            >
              Sign in
            </Link>
          )}
        </div>
      )}

      {/* Loading state */}
      {loading && !error && (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
          {[1, 2, 3, 4].map(i => (
            <div key={i} style={{
              backgroundColor: '#fff', borderRadius: 16, padding: 24,
              border: '1px solid ' + C.border,
            }}>
              <div style={{ height: 14, backgroundColor: '#e5e7eb', borderRadius: 7, width: '30%', marginBottom: 12 }} />
              <div style={{ height: 20, backgroundColor: '#e5e7eb', borderRadius: 7, width: '80%', marginBottom: 8 }} />
              <div style={{ height: 14, backgroundColor: '#e5e7eb', borderRadius: 7, width: '50%' }} />
            </div>
          ))}
        </div>
      )}

      {/* Empty state */}
      {!loading && !error && activity.length === 0 && (
        <div style={{
          backgroundColor: '#fff', borderRadius: 16, padding: 60,
          textAlign: 'center', border: '1px solid ' + C.border,
        }}>
          <FileText size={48} style={{ color: '#d1d5db', marginBottom: 16 }} />
          <h3 style={{ fontSize: 20, fontWeight: 600, color: C.text, margin: '0 0 8px', fontFamily: 'Georgia, serif' }}>
            No activity yet
          </h3>
          <p style={{ fontSize: 15, color: C.textMuted, margin: '0 0 24px', fontFamily: 'system-ui, sans-serif' }}>
            Sign your first petition to see it here.
          </p>
          <Link
            href="/pulse/petitions"
            style={{
              display: 'inline-flex', alignItems: 'center', gap: 8,
              backgroundColor: C.darkGreen, color: '#fff', textDecoration: 'none',
              padding: '12px 28px', borderRadius: 9999, fontSize: 15, fontWeight: 600,
              fontFamily: 'system-ui, sans-serif',
            }}
          >
            Browse Petitions
          </Link>
        </div>
      )}

      {/* Timeline */}
      {!loading && !error && activity.length > 0 && (
        <div style={{ position: 'relative' }}>
          {/* Vertical timeline line */}
          <div style={{
            position: 'absolute', left: 21, top: 24, bottom: 24,
            width: 2, backgroundColor: '#e5e7eb',
          }} />

          <div style={{ display: 'flex', flexDirection: 'column', gap: 0 }}>
            {activity.map((item, idx) => {
              const pct = progressPercent(item.signature_count, item.signature_goal);
              return (
                <div key={item.signature_id} style={{ display: 'flex', gap: 20, position: 'relative' }}>
                  {/* Timeline dot */}
                  <div style={{
                    width: 44, minWidth: 44, display: 'flex', flexDirection: 'column',
                    alignItems: 'center', paddingTop: 24,
                  }}>
                    <div style={{
                      width: 12, height: 12, borderRadius: '50%', zIndex: 1,
                      backgroundColor: item.is_featured ? C.gold : C.darkGreen,
                      border: '3px solid #fff',
                      boxShadow: '0 0 0 2px ' + (item.is_featured ? C.gold : C.darkGreen),
                    }} />
                  </div>

                  {/* Card */}
                  <div style={{
                    flex: 1, backgroundColor: '#fff', borderRadius: 16,
                    border: '1px solid ' + C.border, padding: 24,
                    marginBottom: idx < activity.length - 1 ? 16 : 0,
                    transition: 'box-shadow 200ms',
                  }}>
                    {/* Date + status */}
                    <div style={{
                      display: 'flex', justifyContent: 'space-between', alignItems: 'center',
                      marginBottom: 12, flexWrap: 'wrap', gap: 8,
                    }}>
                      <div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 13, color: C.textMuted, fontFamily: 'system-ui, sans-serif' }}>
                        <Calendar size={14} />
                        Signed {formatDate(item.signed_at)}
                      </div>
                      <div style={{ display: 'flex', gap: 8 }}>
                        <span style={{
                          display: 'inline-block', fontSize: 11, fontWeight: 600,
                          textTransform: 'uppercase', letterSpacing: '0.5px',
                          padding: '3px 10px', borderRadius: 9999,
                          backgroundColor: `${categoryColor(item.category)}15`,
                          color: categoryColor(item.category),
                          fontFamily: 'system-ui, sans-serif',
                        }}>
                          {categoryLabel(item.category)}
                        </span>
                        {item.is_featured && (
                          <span style={{
                            display: 'inline-flex', alignItems: 'center', gap: 4,
                            fontSize: 11, fontWeight: 600, padding: '3px 10px', borderRadius: 9999,
                            backgroundColor: 'rgba(218,165,32,0.15)', color: C.gold,
                            fontFamily: 'system-ui, sans-serif',
                          }}>
                            <Sparkles size={12} />
                            Featured
                          </span>
                        )}
                      </div>
                    </div>

                    {/* Title */}
                    <Link
                      href={`/pulse/petition/${item.petition_id}`}
                      style={{
                        display: 'block', fontSize: 18, fontWeight: 700, lineHeight: '26px',
                        color: C.text, textDecoration: 'none', marginBottom: 12,
                        fontFamily: 'Georgia, serif',
                      }}
                    >
                      {item.petition_title}
                    </Link>

                    {/* Comment */}
                    {item.comment && (
                      <div style={{
                        display: 'flex', gap: 8, padding: '10px 14px', borderRadius: 10,
                        backgroundColor: '#f9fafb', marginBottom: 12,
                        fontSize: 14, color: '#4b5563', fontFamily: 'system-ui, sans-serif',
                        fontStyle: 'italic', lineHeight: '22px',
                      }}>
                        <MessageSquare size={16} style={{ color: '#9ca3af', flexShrink: 0, marginTop: 2 }} />
                        <span>&ldquo;{item.comment}&rdquo;</span>
                      </div>
                    )}

                    {/* Progress */}
                    <div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
                      <div style={{ flex: 1 }}>
                        <div style={{
                          display: 'flex', justifyContent: 'space-between', marginBottom: 4,
                          fontSize: 13, fontFamily: 'system-ui, sans-serif',
                        }}>
                          <span style={{ fontWeight: 600, color: C.text }}>
                            <Target size={13} style={{ display: 'inline', verticalAlign: '-2px', marginRight: 4 }} />
                            {formatNumber(item.signature_count)} signatures
                          </span>
                          {item.signature_goal && (
                            <span style={{ color: C.textMuted }}>
                              {pct}% of {formatNumber(item.signature_goal)}
                            </span>
                          )}
                        </div>
                        <div style={{ height: 6, borderRadius: 3, backgroundColor: '#e5e7eb', overflow: 'hidden' }}>
                          <div style={{
                            height: '100%', borderRadius: 3, backgroundColor: C.gold,
                            width: `${pct}%`, transition: 'width 500ms ease',
                          }} />
                        </div>
                      </div>
                      <span style={{
                        fontSize: 12, fontWeight: 600, fontFamily: 'system-ui, sans-serif',
                        padding: '4px 10px', borderRadius: 9999,
                        backgroundColor: item.status === 'active' ? 'rgba(22,163,106,0.1)' : 'rgba(107,114,128,0.1)',
                        color: item.status === 'active' ? '#16a34a' : '#6b7280',
                        textTransform: 'capitalize',
                      }}>
                        {item.status}
                      </span>
                    </div>
                  </div>
                </div>
              );
            })}
          </div>
        </div>
      )}

      {/* Pagination */}
      {!loading && totalPages > 1 && (
        <div style={{
          display: 'flex', justifyContent: 'center', alignItems: 'center', gap: 16,
          marginTop: 32, fontFamily: 'system-ui, sans-serif',
        }}>
          <button
            onClick={() => setPage(p => Math.max(1, p - 1))}
            disabled={page <= 1}
            style={{
              display: 'inline-flex', alignItems: 'center', gap: 4,
              padding: '8px 16px', borderRadius: 8, fontSize: 14, fontWeight: 500,
              backgroundColor: page <= 1 ? '#f3f4f6' : '#fff',
              border: '1px solid ' + C.border, cursor: page <= 1 ? 'default' : 'pointer',
              color: page <= 1 ? '#9ca3af' : C.text,
              transition: 'all 150ms',
            }}
          >
            <ChevronLeft size={16} />
            Previous
          </button>
          <span style={{ fontSize: 14, color: C.textMuted }}>
            Page {page} of {totalPages}
          </span>
          <button
            onClick={() => setPage(p => Math.min(totalPages, p + 1))}
            disabled={page >= totalPages}
            style={{
              display: 'inline-flex', alignItems: 'center', gap: 4,
              padding: '8px 16px', borderRadius: 8, fontSize: 14, fontWeight: 500,
              backgroundColor: page >= totalPages ? '#f3f4f6' : '#fff',
              border: '1px solid ' + C.border, cursor: page >= totalPages ? 'default' : 'pointer',
              color: page >= totalPages ? '#9ca3af' : C.text,
              transition: 'all 150ms',
            }}
          >
            Next
            <ChevronRight size={16} />
          </button>
        </div>
      )}
    </div>
  );
}