← back to Norma

app/pulse/petitions/page.tsx

456 lines

'use client';

import React, { useEffect, useState, useCallback, Suspense } from 'react';
import Link from 'next/link';
import { useSearchParams } from 'next/navigation';
import {
  Search, SlidersHorizontal, ChevronLeft, ChevronRight,
  FileText, Target, Sparkles, Tag, ArrowUpDown, Loader2, Plus,
} from 'lucide-react';

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

interface Petition {
  id: string;
  title: string;
  body: string | null;
  description: string | null;
  target: string | null;
  category: string | null;
  signature_count: number | null;
  signature_goal: number | null;
  is_featured: boolean;
  tags: string[] | null;
  created_at: string;
}

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

const C = {
  darkGreen: '#1B4332',
  gold: '#DAA520',
  cream: '#FAFAF5',
  text: '#1a1a1a',
  textLight: '#fafafa',
  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 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 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 timeAgo(dateStr: string): string {
  const diff = Date.now() - new Date(dateStr).getTime();
  const days = Math.floor(diff / 86400000);
  if (days === 0) return 'Today';
  if (days === 1) return 'Yesterday';
  if (days < 30) return `${days} days ago`;
  if (days < 365) return `${Math.floor(days / 30)} months ago`;
  return `${Math.floor(days / 365)} years ago`;
}

const CATEGORIES = [
  'debt_cancellation', 'consumer_protection', 'voting_rights', 'legal',
  'trending', 'education', 'environment', 'healthcare',
];

const SORT_OPTIONS = [
  { value: 'featured', label: 'Featured' },
  { value: 'newest', label: 'Newest' },
  { value: 'signatures', label: 'Most Signatures' },
  { value: 'trending', label: 'Trending' },
];

const PAGE_SIZE = 12;

/* ────────────────────────── Petition Card ────────────────────────── */

function PetitionCard({ petition }: { petition: Petition }) {
  const preview = (petition.body || petition.description || '').slice(0, 130);
  const pct = progressPercent(petition.signature_count, petition.signature_goal);

  return (
    <Link href={`/pulse/petition/${petition.id}`} style={{ textDecoration: 'none', color: 'inherit' }}>
      <div
        style={{
          backgroundColor: '#fff', borderRadius: 16, overflow: 'hidden',
          boxShadow: '0 1px 3px rgba(0,0,0,0.08)',
          transition: 'transform 200ms, box-shadow 200ms', cursor: 'pointer',
          display: 'flex', flexDirection: 'column', height: '100%',
          border: petition.is_featured ? `2px solid ${C.gold}` : '1px solid #e5e7eb',
        }}
        onMouseEnter={e => { e.currentTarget.style.transform = 'translateY(-3px)'; e.currentTarget.style.boxShadow = '0 8px 20px rgba(0,0,0,0.1)'; }}
        onMouseLeave={e => { e.currentTarget.style.transform = 'translateY(0)'; e.currentTarget.style.boxShadow = '0 1px 3px rgba(0,0,0,0.08)'; }}
      >
        {petition.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: 6 }}>
            <Sparkles size={12} /> FEATURED
          </div>
        )}

        <div style={{ padding: 20, flex: 1, display: 'flex', flexDirection: 'column' }}>
          {/* Top row: category + time */}
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 }}>
            <span style={{
              fontSize: 11, fontWeight: 600, fontFamily: 'system-ui, sans-serif',
              textTransform: 'uppercase', letterSpacing: '0.5px',
              padding: '3px 8px', borderRadius: 9999,
              backgroundColor: `${categoryColor(petition.category)}12`,
              color: categoryColor(petition.category),
            }}>
              {categoryLabel(petition.category)}
            </span>
            <span style={{ fontSize: 12, color: C.textMuted, fontFamily: 'system-ui, sans-serif' }}>
              {timeAgo(petition.created_at)}
            </span>
          </div>

          {/* Title */}
          <h3 style={{ fontSize: 17, fontWeight: 700, lineHeight: '24px', margin: '0 0 8px', color: C.text, fontFamily: 'Georgia, serif' }}>
            {petition.title}
          </h3>

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

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

          {/* Progress */}
          <div>
            <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 5, fontSize: 13, fontFamily: 'system-ui, sans-serif' }}>
              <span style={{ fontWeight: 600, color: C.text }}>
                {formatNumber(petition.signature_count)} signed
              </span>
              {petition.signature_goal && petition.signature_goal > 0 && (
                <span style={{ color: C.textMuted, fontSize: 12 }}>
                  {pct}% of {formatNumber(petition.signature_goal)}
                </span>
              )}
            </div>
            <div style={{ height: 5, borderRadius: 3, backgroundColor: '#e5e7eb', overflow: 'hidden' }}>
              <div style={{
                height: '100%', borderRadius: 3, backgroundColor: C.gold,
                width: `${petition.signature_goal ? pct : Math.min(100, (petition.signature_count || 0) / 100)}%`,
                minWidth: (petition.signature_count || 0) > 0 ? 8 : 0,
                transition: 'width 500ms ease',
              }} />
            </div>
          </div>
        </div>
      </div>
    </Link>
  );
}

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

function PetitionsListInner() {
  const searchParams = useSearchParams();
  const initialCategory = searchParams.get('category') || '';

  const [petitions, setPetitions] = useState<Petition[]>([]);
  const [total, setTotal] = useState(0);
  const [loading, setLoading] = useState(true);
  const [page, setPage] = useState(0);
  const [searchTerm, setSearchTerm] = useState('');
  const [category, setCategory] = useState(initialCategory);
  const [sort, setSort] = useState('featured');
  const [filtersOpen, setFiltersOpen] = useState(false);

  const totalPages = Math.ceil(total / PAGE_SIZE);

  const loadPetitions = useCallback(async () => {
    setLoading(true);
    try {
      const params = new URLSearchParams();
      params.set('limit', String(PAGE_SIZE));
      params.set('offset', String(page * PAGE_SIZE));
      params.set('sort', sort);
      if (category) params.set('category', category);
      if (searchTerm) params.set('search', searchTerm);

      const res = await fetch(`/api/pulse/petitions?${params.toString()}`);
      if (res.ok) {
        const data = await res.json();
        setPetitions(data.petitions || []);
        setTotal(data.total || 0);
      }
    } catch (err) {
      console.error('Failed to load petitions:', err);
    } finally {
      setLoading(false);
    }
  }, [page, sort, category, searchTerm]);

  useEffect(() => {
    loadPetitions();
  }, [loadPetitions]);

  // Reset page when filters change
  useEffect(() => {
    setPage(0);
  }, [sort, category, searchTerm]);

  const handleSearch = (e: React.FormEvent) => {
    e.preventDefault();
    setPage(0);
    loadPetitions();
  };

  return (
    <div style={{ maxWidth: 1200, margin: '0 auto', padding: '32px 24px 64px' }}>
      {/* Header */}
      <div style={{ marginBottom: 32, display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', flexWrap: 'wrap', gap: 16 }}>
        <div>
          <h1 style={{ fontSize: 'clamp(28px, 4vw, 42px)', fontWeight: 'bold', color: C.text, margin: '0 0 8px', fontFamily: 'Georgia, serif' }}>
            Active Petitions
          </h1>
          <p style={{ fontSize: 16, color: C.textMuted, margin: 0, fontFamily: 'system-ui, sans-serif' }}>
            {total} petition{total !== 1 ? 's' : ''} ready for your voice
          </p>
        </div>
        <Link
          href="/pulse/petitions/create"
          style={{
            display: 'inline-flex', alignItems: 'center', gap: 8,
            backgroundColor: C.gold, color: C.darkGreen, textDecoration: 'none',
            fontSize: 15, fontWeight: 700, fontFamily: 'system-ui, sans-serif',
            padding: '12px 24px', borderRadius: 9999,
            transition: 'transform 150ms, background-color 150ms',
            boxShadow: '0 2px 8px rgba(218,165,32,0.25)',
            flexShrink: 0,
          }}
          onMouseEnter={e => { e.currentTarget.style.transform = 'scale(1.03)'; e.currentTarget.style.backgroundColor = '#c8941a'; }}
          onMouseLeave={e => { e.currentTarget.style.transform = 'scale(1)'; e.currentTarget.style.backgroundColor = '#DAA520'; }}
        >
          <Plus size={18} />
          Create Petition
        </Link>
      </div>

      {/* Filter Bar */}
      <div style={{
        backgroundColor: '#fff', borderRadius: 16, padding: 20, marginBottom: 24,
        border: '1px solid #e5e7eb', boxShadow: '0 1px 2px rgba(0,0,0,0.04)',
      }}>
        <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', alignItems: 'center' }}>
          {/* Search */}
          <form onSubmit={handleSearch} style={{ flex: '1 1 280px', position: 'relative' }}>
            <Search size={18} style={{ position: 'absolute', left: 14, top: '50%', transform: 'translateY(-50%)', color: C.textMuted }} />
            <input
              type="text"
              placeholder="Search petitions..."
              value={searchTerm}
              onChange={e => setSearchTerm(e.target.value)}
              style={{
                width: '100%', padding: '12px 16px 12px 42px', borderRadius: 12,
                border: '1px solid #d1d5db', backgroundColor: C.cream, fontSize: 14,
                fontFamily: 'system-ui, sans-serif', outline: 'none', color: C.text,
                boxSizing: 'border-box',
              }}
              onFocus={e => (e.target.style.borderColor = C.darkGreen)}
              onBlur={e => (e.target.style.borderColor = '#d1d5db')}
            />
          </form>

          {/* Sort dropdown */}
          <div style={{ position: 'relative', flex: '0 0 auto' }}>
            <ArrowUpDown size={15} style={{ position: 'absolute', left: 12, top: '50%', transform: 'translateY(-50%)', color: C.textMuted, pointerEvents: 'none' }} />
            <select
              value={sort}
              onChange={e => setSort(e.target.value)}
              style={{
                padding: '12px 16px 12px 34px', borderRadius: 12,
                border: '1px solid #d1d5db', backgroundColor: C.cream, fontSize: 14,
                fontFamily: 'system-ui, sans-serif', color: C.text, cursor: 'pointer',
                appearance: 'none', paddingRight: 32, outline: 'none',
              }}
            >
              {SORT_OPTIONS.map(opt => (
                <option key={opt.value} value={opt.value}>{opt.label}</option>
              ))}
            </select>
          </div>

          {/* Filter toggle */}
          <button
            onClick={() => setFiltersOpen(!filtersOpen)}
            style={{
              display: 'flex', alignItems: 'center', gap: 6, padding: '12px 18px',
              borderRadius: 12, border: `1px solid ${filtersOpen ? C.darkGreen : '#d1d5db'}`,
              backgroundColor: filtersOpen ? `${C.darkGreen}08` : 'transparent',
              color: filtersOpen ? C.darkGreen : C.textMuted,
              cursor: 'pointer', fontSize: 14, fontFamily: 'system-ui, sans-serif',
              fontWeight: 500,
            }}
          >
            <SlidersHorizontal size={16} />
            Filters
            {category && <span style={{ width: 8, height: 8, borderRadius: '50%', backgroundColor: C.gold }} />}
          </button>
        </div>

        {/* Expanded filters */}
        {filtersOpen && (
          <div style={{ marginTop: 16, paddingTop: 16, borderTop: '1px solid #e5e7eb' }}>
            <div style={{ fontSize: 13, fontWeight: 600, color: C.textMuted, marginBottom: 10, fontFamily: 'system-ui, sans-serif', textTransform: 'uppercase', letterSpacing: 0.5 }}>
              Category
            </div>
            <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
              <button
                onClick={() => setCategory('')}
                style={{
                  padding: '7px 16px', borderRadius: 9999, fontSize: 13,
                  fontFamily: 'system-ui, sans-serif', fontWeight: 500, cursor: 'pointer',
                  border: !category ? `2px solid ${C.darkGreen}` : '1px solid #d1d5db',
                  backgroundColor: !category ? C.darkGreen : 'transparent',
                  color: !category ? '#fff' : C.text,
                }}
              >
                All
              </button>
              {CATEGORIES.map(cat => (
                <button
                  key={cat}
                  onClick={() => setCategory(category === cat ? '' : cat)}
                  style={{
                    padding: '7px 16px', borderRadius: 9999, fontSize: 13,
                    fontFamily: 'system-ui, sans-serif', fontWeight: 500, cursor: 'pointer',
                    border: category === cat ? `2px solid ${categoryColor(cat)}` : '1px solid #d1d5db',
                    backgroundColor: category === cat ? `${categoryColor(cat)}12` : 'transparent',
                    color: category === cat ? categoryColor(cat) : C.text,
                  }}
                >
                  {categoryLabel(cat)}
                </button>
              ))}
            </div>
          </div>
        )}
      </div>

      {/* Petition Grid */}
      {loading ? (
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))', gap: 20 }}>
          {[1, 2, 3, 4, 5, 6].map(i => (
            <div key={i} style={{ backgroundColor: '#fff', borderRadius: 16, height: 260, border: '1px solid #e5e7eb', padding: 20 }}>
              <div style={{ height: 12, backgroundColor: '#e5e7eb', borderRadius: 6, width: '30%', marginBottom: 14 }} />
              <div style={{ height: 18, backgroundColor: '#e5e7eb', borderRadius: 6, width: '90%', marginBottom: 8 }} />
              <div style={{ height: 18, backgroundColor: '#e5e7eb', borderRadius: 6, width: '65%', marginBottom: 16 }} />
              <div style={{ height: 13, backgroundColor: '#e5e7eb', borderRadius: 6, width: '100%', marginBottom: 6 }} />
              <div style={{ height: 13, backgroundColor: '#e5e7eb', borderRadius: 6, width: '75%', marginBottom: 20 }} />
              <div style={{ height: 5, backgroundColor: '#e5e7eb', borderRadius: 3, width: '100%' }} />
            </div>
          ))}
        </div>
      ) : petitions.length === 0 ? (
        <div style={{ textAlign: 'center', padding: '60px 20px', backgroundColor: '#fff', borderRadius: 16, border: '1px solid #e5e7eb' }}>
          <FileText size={48} style={{ color: C.textMuted, opacity: 0.3, marginBottom: 16 }} />
          <h3 style={{ fontSize: 20, color: C.text, margin: '0 0 8px', fontFamily: 'Georgia, serif' }}>No petitions found</h3>
          <p style={{ fontSize: 15, color: C.textMuted, fontFamily: 'system-ui, sans-serif' }}>
            Try adjusting your filters or search terms
          </p>
        </div>
      ) : (
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))', gap: 20 }}>
          {petitions.map(p => <PetitionCard key={p.id} petition={p} />)}
        </div>
      )}

      {/* Pagination */}
      {totalPages > 1 && (
        <div style={{
          display: 'flex', justifyContent: 'center', alignItems: 'center', gap: 16,
          marginTop: 40, fontFamily: 'system-ui, sans-serif',
        }}>
          <button
            onClick={() => setPage(Math.max(0, page - 1))}
            disabled={page === 0}
            style={{
              display: 'flex', alignItems: 'center', gap: 6, padding: '10px 20px',
              borderRadius: 12, border: '1px solid #d1d5db', backgroundColor: '#fff',
              color: page === 0 ? '#d1d5db' : C.text, cursor: page === 0 ? 'not-allowed' : 'pointer',
              fontSize: 14, fontWeight: 500,
            }}
          >
            <ChevronLeft size={16} />
            Previous
          </button>

          <span style={{ fontSize: 14, color: C.textMuted }}>
            Page {page + 1} of {totalPages}
          </span>

          <button
            onClick={() => setPage(Math.min(totalPages - 1, page + 1))}
            disabled={page >= totalPages - 1}
            style={{
              display: 'flex', alignItems: 'center', gap: 6, padding: '10px 20px',
              borderRadius: 12, border: '1px solid #d1d5db', backgroundColor: '#fff',
              color: page >= totalPages - 1 ? '#d1d5db' : C.text,
              cursor: page >= totalPages - 1 ? 'not-allowed' : 'pointer',
              fontSize: 14, fontWeight: 500,
            }}
          >
            Next
            <ChevronRight size={16} />
          </button>
        </div>
      )}
    </div>
  );
}

/* ─── Default export wraps inner component in Suspense for useSearchParams ─── */

export default function PetitionsListPage() {
  return (
    <Suspense fallback={
      <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: 400 }}>
        <Loader2 size={32} style={{ color: '#DAA520', animation: 'spin 1s linear infinite' }} />
        <style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style>
      </div>
    }>
      <PetitionsListInner />
    </Suspense>
  );
}