← back to Norma

app/pulse/tracked/page.tsx

386 lines

'use client';

import React, { useEffect, useState, useCallback } from 'react';
import Link from 'next/link';
import {
  Bookmark, BookmarkMinus, FileText, Target, Sparkles,
  ChevronLeft, ChevronRight, RefreshCw,
} from 'lucide-react';

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

interface TrackedPetition {
  tracking_id: string;
  tracked_at: string;
  petition_id: string;
  title: string;
  description: string | null;
  body: string | null;
  category: string | null;
  status: string;
  target: string | null;
  signature_count: number | null;
  signature_goal: number | null;
  is_featured: boolean;
  tags: string[] | null;
  petition_updated_at: string;
}

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

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

/* ────────────────────────── 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 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));
}

function timeAgo(dateStr: string): string {
  const now = Date.now();
  const d = new Date(dateStr).getTime();
  const diff = now - d;
  const mins = Math.floor(diff / 60000);
  if (mins < 1) return 'just now';
  if (mins < 60) return `${mins}m ago`;
  const hours = Math.floor(mins / 60);
  if (hours < 24) return `${hours}h ago`;
  const days = Math.floor(hours / 24);
  if (days < 30) return `${days}d ago`;
  const months = Math.floor(days / 30);
  return `${months}mo ago`;
}

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

export default function PulseTrackedPage() {
  const [tracked, setTracked] = useState<TrackedPetition[]>([]);
  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 [untracking, setUntracking] = useState<Set<string>>(new Set());

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

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

  async function handleUntrack(petitionId: string) {
    setUntracking(prev => new Set(prev).add(petitionId));
    try {
      const res = await fetch('/api/pulse/tracked', {
        method: 'DELETE',
        headers: { 'Content-Type': 'application/json' },
        credentials: 'include',
        body: JSON.stringify({ petition_id: petitionId }),
      });
      if (res.ok) {
        setTracked(prev => prev.filter(t => t.petition_id !== petitionId));
        setTotal(prev => prev - 1);
      }
    } catch {
      // Silently fail
    } finally {
      setUntracking(prev => { const s = new Set(prev); s.delete(petitionId); return s; });
    }
  }

  return (
    <div style={{ maxWidth: 1100, margin: '0 auto', padding: '40px 24px 80px' }}>
      {/* Header */}
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 32, flexWrap: 'wrap', gap: 12 }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
          <div style={{
            width: 44, height: 44, borderRadius: 12,
            backgroundColor: 'rgba(218,165,32,0.1)',
            display: 'flex', alignItems: 'center', justifyContent: 'center',
          }}>
            <Bookmark size={22} style={{ color: C.gold }} />
          </div>
          <div>
            <h1 style={{ fontSize: 28, fontWeight: 'bold', color: C.text, margin: 0, fontFamily: 'Georgia, serif' }}>
              Tracked Petitions
            </h1>
            <p style={{ fontSize: 14, color: C.textMuted, margin: '4px 0 0', fontFamily: 'system-ui, sans-serif' }}>
              {total > 0 ? `Tracking ${total} petition${total === 1 ? '' : 's'}` : 'Petitions you are following'}
            </p>
          </div>
        </div>
        <button
          onClick={() => fetchTracked(page)}
          style={{
            display: 'inline-flex', alignItems: 'center', gap: 6,
            padding: '8px 16px', borderRadius: 8, fontSize: 14, fontWeight: 500,
            backgroundColor: '#fff', border: '1px solid ' + C.border, cursor: 'pointer',
            color: C.text, fontFamily: 'system-ui, sans-serif', transition: 'all 150ms',
          }}
        >
          <RefreshCw size={14} />
          Refresh
        </button>
      </div>

      {/* Error state */}
      {error && (
        <div style={{
          backgroundColor: '#fef2f2', border: '1px solid #fecaca', borderRadius: 12,
          padding: 24, textAlign: 'center', color: '#991b1b',
          fontFamily: 'system-ui, sans-serif',
        }}>
          {error}
        </div>
      )}

      {/* Loading state */}
      {loading && !error && (
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(340px, 1fr))', gap: 20 }}>
          {[1, 2, 3, 4].map(i => (
            <div key={i} style={{
              backgroundColor: '#fff', borderRadius: 16, padding: 24, height: 260,
              border: '1px solid ' + C.border,
            }}>
              <div style={{ height: 14, backgroundColor: '#e5e7eb', borderRadius: 7, width: '40%', marginBottom: 16 }} />
              <div style={{ height: 20, backgroundColor: '#e5e7eb', borderRadius: 7, width: '90%', marginBottom: 8 }} />
              <div style={{ height: 14, backgroundColor: '#e5e7eb', borderRadius: 7, width: '60%', marginBottom: 20 }} />
              <div style={{ height: 6, backgroundColor: '#e5e7eb', borderRadius: 3, width: '100%' }} />
            </div>
          ))}
        </div>
      )}

      {/* Empty state */}
      {!loading && !error && tracked.length === 0 && (
        <div style={{
          backgroundColor: '#fff', borderRadius: 16, padding: 60,
          textAlign: 'center', border: '1px solid ' + C.border,
        }}>
          <Bookmark size={48} style={{ color: '#d1d5db', marginBottom: 16 }} />
          <h3 style={{ fontSize: 20, fontWeight: 600, color: C.text, margin: '0 0 8px', fontFamily: 'Georgia, serif' }}>
            No tracked petitions
          </h3>
          <p style={{ fontSize: 15, color: C.textMuted, margin: '0 0 24px', fontFamily: 'system-ui, sans-serif' }}>
            Track petitions to follow their progress and get updates.
          </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>
      )}

      {/* Card grid */}
      {!loading && !error && tracked.length > 0 && (
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(340px, 1fr))', gap: 20 }}>
          {tracked.map(item => {
            const pct = progressPercent(item.signature_count, item.signature_goal);
            const preview = (item.body || item.description || '').slice(0, 100);
            const isRemoving = untracking.has(item.petition_id);

            return (
              <div
                key={item.tracking_id}
                style={{
                  backgroundColor: '#fff', borderRadius: 16, overflow: 'hidden',
                  border: item.is_featured ? `2px solid ${C.gold}` : '1px solid ' + C.border,
                  display: 'flex', flexDirection: 'column',
                  transition: 'transform 200ms, box-shadow 200ms, opacity 300ms',
                  opacity: isRemoving ? 0.5 : 1,
                }}
              >
                {/* Featured badge */}
                {item.is_featured && (
                  <div style={{
                    backgroundColor: C.gold, color: C.darkGreen,
                    padding: '5px 16px', fontSize: 11, fontWeight: 700,
                    fontFamily: 'system-ui, sans-serif',
                    display: 'flex', alignItems: 'center', gap: 5,
                  }}>
                    <Sparkles size={12} />
                    FEATURED
                  </div>
                )}

                <div style={{ padding: 24, flex: 1, display: 'flex', flexDirection: 'column' }}>
                  {/* Category + updated */}
                  <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12, flexWrap: 'wrap', gap: 6 }}>
                    <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>
                    <span style={{ fontSize: 12, color: C.textMuted, fontFamily: 'system-ui, sans-serif' }}>
                      Updated {timeAgo(item.petition_updated_at)}
                    </span>
                  </div>

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

                  {/* Preview */}
                  {preview && (
                    <p style={{ fontSize: 14, lineHeight: '21px', color: C.textMuted, margin: '0 0 12px', fontFamily: 'system-ui, sans-serif', flex: 1 }}>
                      {preview}{preview.length >= 100 ? '...' : ''}
                    </p>
                  )}

                  {/* Target */}
                  {item.target && (
                    <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 14, fontSize: 13, color: C.darkGreen, fontFamily: 'system-ui, sans-serif' }}>
                      <Target size={14} />
                      <span style={{ fontWeight: 500 }}>{item.target}</span>
                    </div>
                  )}

                  {/* Progress bar */}
                  <div style={{ marginBottom: 16 }}>
                    <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 5, fontSize: 13, fontFamily: 'system-ui, sans-serif' }}>
                      <span style={{ fontWeight: 600, color: C.text }}>
                        {formatNumber(item.signature_count)} signatures
                      </span>
                      {item.signature_goal && item.signature_goal > 0 && (
                        <span style={{ color: C.textMuted }}>{pct}%</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>

                  {/* Untrack button */}
                  <button
                    onClick={() => handleUntrack(item.petition_id)}
                    disabled={isRemoving}
                    style={{
                      display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 6,
                      padding: '8px 0', borderRadius: 8, fontSize: 13, fontWeight: 600,
                      backgroundColor: 'transparent', border: '1px solid #fca5a5',
                      color: '#dc2626', cursor: isRemoving ? 'default' : 'pointer',
                      fontFamily: 'system-ui, sans-serif', transition: 'all 150ms',
                      width: '100%',
                    }}
                  >
                    <BookmarkMinus size={14} />
                    {isRemoving ? 'Removing...' : 'Untrack'}
                  </button>
                </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,
            }}
          >
            <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,
            }}
          >
            Next
            <ChevronRight size={16} />
          </button>
        </div>
      )}
    </div>
  );
}