← back to Norma
app/pulse/page.tsx
784 lines
'use client';
import React, { useEffect, useState, useCallback } from 'react';
import Link from 'next/link';
import {
ArrowRight, Users, FileText, Tag, TrendingUp,
ChevronRight, Target, Sparkles, Bookmark, BookmarkCheck,
Activity, User, LogIn,
} 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;
}
interface TopicCategory {
category: string;
count: number;
}
interface SessionInfo {
authenticated: boolean;
user?: string;
role?: string;
}
/* ────────────────────────── Rolling Brand Animation ─────── */
const ROLL_PHRASES = ['Your Voice', 'Your Power', 'Your Petition', 'Your Future'];
const ROLL_INTERVAL = 2800;
function RollingText() {
const [idx, setIdx] = useState(0);
const [animKey, setAnimKey] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setIdx(prev => (prev + 1) % ROLL_PHRASES.length);
setAnimKey(prev => prev + 1);
}, ROLL_INTERVAL);
return () => clearInterval(timer);
}, []);
return (
<span style={{ display: 'inline-block', perspective: 600, verticalAlign: 'bottom' }}>
<span
key={animKey}
style={{
display: 'inline-block',
animation: `poaRollIn 0.7s cubic-bezier(0.23, 1, 0.32, 1) forwards`,
color: '#DAA520',
willChange: 'transform, opacity',
}}
>
{ROLL_PHRASES[idx]}
</span>
<style>{`
@keyframes poaRollIn {
0% { opacity: 0; transform: rotateX(-90deg) translateY(20px); }
30% { opacity: 0.6; }
100% { opacity: 1; transform: rotateX(0deg) translateY(0); }
}
@keyframes poaPulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(218,165,32,0.4); }
50% { box-shadow: 0 0 20px 8px rgba(218,165,32,0.15); }
}
@keyframes poaGlow {
0%, 100% { text-shadow: 0 0 10px rgba(218,165,32,0.3); }
50% { text-shadow: 0 0 30px rgba(218,165,32,0.6), 0 0 60px rgba(218,165,32,0.2); }
}
@keyframes poaFloat {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-4px); }
}
@keyframes poaMarquee {
0% { transform: translateX(0); }
100% { transform: translateX(-50%); }
}
`}</style>
</span>
);
}
function POABadge() {
return (
<span style={{
display: 'inline-flex', alignItems: 'center', gap: 8,
padding: '8px 20px', borderRadius: 9999,
background: 'linear-gradient(135deg, rgba(218,165,32,0.2), rgba(218,165,32,0.05))',
border: '1px solid rgba(218,165,32,0.4)',
animation: 'poaPulse 3s ease-in-out infinite, poaFloat 4s ease-in-out infinite',
fontFamily: 'Georgia, serif', fontWeight: 700, fontSize: 14,
letterSpacing: 3, color: '#DAA520', textTransform: 'uppercase',
}}>
<span style={{
width: 8, height: 8, borderRadius: '50%',
backgroundColor: '#DAA520',
animation: 'poaPulse 2s ease-in-out infinite',
}} />
Pulse of America
</span>
);
}
function BrandMarquee() {
const items = ['POA', '\u2605', 'PULSE OF AMERICA', '\u2605', 'YOUR VOICE MATTERS', '\u2605', 'CIVIC ACTION', '\u2605'];
const doubled = [...items, ...items];
return (
<div style={{
overflow: 'hidden', width: '100%', position: 'absolute', bottom: 0,
borderTop: '1px solid rgba(218,165,32,0.15)',
backgroundColor: 'rgba(0,0,0,0.1)',
padding: '8px 0',
}}>
<div style={{
display: 'flex', gap: 40, whiteSpace: 'nowrap',
animation: 'poaMarquee 30s linear infinite',
width: 'max-content',
}}>
{doubled.map((item, i) => (
<span key={i} style={{
fontSize: 11, fontWeight: 700, letterSpacing: 4,
color: 'rgba(218,165,32,0.4)', fontFamily: 'system-ui, sans-serif',
textTransform: 'uppercase',
}}>
{item}
</span>
))}
</div>
</div>
);
}
/* ────────────────────────── Colors ────────────────────────── */
const C = {
darkGreen: '#1B4332',
gold: '#DAA520',
cream: '#FAFAF5',
text: '#1a1a1a',
textLight: '#fafafa',
textMuted: '#6b7280',
border: '#e5e7eb',
greenLight: '#d1fae5',
};
/* ────────────────────────── 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;
const c = count || 0;
return Math.min(100, Math.round((c / 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();
}
/* ────────────────────────── Petition Card ────────────────────────── */
function PetitionCard({
petition,
isTracked,
onTrack,
isAuthenticated,
}: {
petition: Petition;
isTracked: boolean;
onTrack: (id: string, track: boolean) => void;
isAuthenticated: boolean;
}) {
const preview = (petition.body || petition.description || '').slice(0, 120);
const pct = progressPercent(petition.signature_count, petition.signature_goal);
const [tracking, setTracking] = useState(false);
async function handleTrack(e: React.MouseEvent) {
e.preventDefault();
e.stopPropagation();
if (tracking) return;
setTracking(true);
try {
onTrack(petition.id, !isTracked);
} finally {
setTracking(false);
}
}
return (
<div
style={{
backgroundColor: '#fff', borderRadius: 16, overflow: 'hidden',
boxShadow: '0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.06)',
transition: 'transform 200ms, box-shadow 200ms',
display: 'flex', flexDirection: 'column', height: '100%',
border: petition.is_featured ? `2px solid ${C.gold}` : '1px solid #e5e7eb',
}}
onMouseEnter={e => { e.currentTarget.style.transform = 'translateY(-4px)'; e.currentTarget.style.boxShadow = '0 10px 25px 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)'; }}
>
{/* Featured badge */}
{petition.is_featured && (
<div style={{ backgroundColor: C.gold, color: C.darkGreen, padding: '6px 16px', fontSize: 12, fontWeight: 700, fontFamily: 'system-ui, sans-serif', display: 'flex', alignItems: 'center', gap: 6 }}>
<Sparkles size={14} />
FEATURED PETITION
</div>
)}
<div style={{ padding: 24, flex: 1, display: 'flex', flexDirection: 'column' }}>
{/* Category badge + Track button */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
<span style={{
display: 'inline-block', fontSize: 11, fontWeight: 600,
fontFamily: 'system-ui, sans-serif', textTransform: 'uppercase',
letterSpacing: '0.5px', padding: '4px 10px', borderRadius: 9999,
backgroundColor: `${categoryColor(petition.category)}15`,
color: categoryColor(petition.category),
}}>
{categoryLabel(petition.category)}
</span>
{isAuthenticated && (
<button
onClick={handleTrack}
disabled={tracking}
title={isTracked ? 'Untrack petition' : 'Track petition'}
style={{
display: 'inline-flex', alignItems: 'center', gap: 4,
padding: '4px 10px', borderRadius: 6, fontSize: 12, fontWeight: 600,
fontFamily: 'system-ui, sans-serif',
backgroundColor: isTracked ? 'rgba(218,165,32,0.1)' : 'transparent',
border: `1px solid ${isTracked ? C.gold : C.border}`,
color: isTracked ? C.gold : C.textMuted,
cursor: 'pointer', transition: 'all 150ms',
}}
>
{isTracked ? <BookmarkCheck size={13} /> : <Bookmark size={13} />}
{isTracked ? 'Tracked' : 'Track'}
</button>
)}
</div>
{/* Title */}
<h3 style={{ fontSize: 18, fontWeight: 700, lineHeight: '26px', margin: '0 0 8px', color: C.text, fontFamily: 'Georgia, serif' }}>
<Link href={`/pulse/petition/${petition.id}`} style={{ textDecoration: 'none', color: 'inherit' }}>
{petition.title}
</Link>
</h3>
{/* Preview */}
<p style={{ fontSize: 14, lineHeight: '22px', color: C.textMuted, margin: '0 0 12px', fontFamily: 'system-ui, sans-serif', flex: 1 }}>
{preview}{preview.length >= 120 ? '...' : ''}
</p>
{/* Target */}
{petition.target && (
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 16, fontSize: 13, color: C.darkGreen, fontFamily: 'system-ui, sans-serif' }}>
<Target size={14} />
<span style={{ fontWeight: 500 }}>{petition.target}</span>
</div>
)}
{/* Progress */}
<div>
{petition.signature_goal && petition.signature_goal > 0 ? (
<>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6, fontSize: 13, fontFamily: 'system-ui, sans-serif' }}>
<span style={{ fontWeight: 600, color: C.text }}>
{formatNumber(petition.signature_count)} signatures
</span>
<span style={{ color: C.textMuted }}>
Goal: {formatNumber(petition.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 style={{ fontSize: 13, fontWeight: 600, color: C.text, fontFamily: 'system-ui, sans-serif' }}>
{formatNumber(petition.signature_count)} signatures
</div>
)}
</div>
</div>
</div>
);
}
/* ────────────────────────── Quick Links Bar ────────────────────────── */
function QuickLinksBar({ session }: { session: SessionInfo }) {
if (!session.authenticated) return null;
return (
<section style={{
maxWidth: 1200, margin: '0 auto', padding: '0 24px',
}}>
<div style={{
display: 'flex', gap: 12, padding: '20px 0',
flexWrap: 'wrap', justifyContent: 'center',
}}>
{[
{ href: '/pulse/activity', label: 'My Activity', icon: Activity, color: C.darkGreen },
{ href: '/pulse/tracked', label: 'Tracked Petitions', icon: Bookmark, color: C.gold },
{ href: '/pulse/profile', label: 'My Profile', icon: User, color: '#6366f1' },
].map(link => (
<Link
key={link.href}
href={link.href}
style={{
display: 'inline-flex', alignItems: 'center', gap: 8,
padding: '10px 20px', borderRadius: 10,
backgroundColor: '#fff', border: '1px solid ' + C.border,
color: C.text, textDecoration: 'none',
fontSize: 14, fontWeight: 500, fontFamily: 'system-ui, sans-serif',
transition: 'all 150ms',
boxShadow: '0 1px 2px rgba(0,0,0,0.04)',
}}
onMouseEnter={e => { e.currentTarget.style.borderColor = link.color; e.currentTarget.style.backgroundColor = '#fafafa'; }}
onMouseLeave={e => { e.currentTarget.style.borderColor = C.border; e.currentTarget.style.backgroundColor = '#fff'; }}
>
<link.icon size={16} style={{ color: link.color }} />
{link.label}
</Link>
))}
</div>
</section>
);
}
/* ────────────────────────── Main Page ────────────────────────── */
export default function PulseLandingPage() {
const [petitions, setPetitions] = useState<Petition[]>([]);
const [topics, setTopics] = useState<TopicCategory[]>([]);
const [loading, setLoading] = useState(true);
const [stats, setStats] = useState({ signatures: 0, petitions: 0, topics: 0 });
const [session, setSession] = useState<SessionInfo>({ authenticated: false });
const [trackedIds, setTrackedIds] = useState<Set<string>>(new Set());
const [categoryAffinity, setCategoryAffinity] = useState<Record<string, number>>({});
// Load session and personalization data
useEffect(() => {
fetch('/api/auth/session', { credentials: 'include' })
.then(r => r.json())
.then(data => {
if (data.authenticated) {
setSession({ authenticated: true, user: data.user, role: data.role });
// Load tracked petitions and activity for personalization
Promise.all([
fetch('/api/pulse/tracked?page=1&limit=100', { credentials: 'include' }).then(r => r.ok ? r.json() : { tracked: [] }),
fetch('/api/pulse/activity?page=1&limit=100', { credentials: 'include' }).then(r => r.ok ? r.json() : { activity: [] }),
]).then(([trackedData, activityData]) => {
// Build tracked set
const ids = new Set<string>((trackedData.tracked || []).map((t: { petition_id: string }) => t.petition_id));
setTrackedIds(ids);
// Build category affinity from signed + tracked petitions
const affinity: Record<string, number> = {};
for (const t of (trackedData.tracked || [])) {
const cat = t.category || 'other';
affinity[cat] = (affinity[cat] || 0) + 1;
}
for (const a of (activityData.activity || [])) {
const cat = a.category || 'other';
affinity[cat] = (affinity[cat] || 0) + 2; // signatures weigh more
}
setCategoryAffinity(affinity);
}).catch(() => {});
}
})
.catch(() => {});
}, []);
// Load petitions and topics
useEffect(() => {
async function load() {
try {
const [petRes, topicRes] = await Promise.all([
fetch('/api/pulse/petitions?limit=12&sort=featured'),
fetch('/api/petitions/topics?limit=12&source=pulse&org=false'),
]);
if (petRes.ok) {
const petData = await petRes.json();
setPetitions(petData.petitions || []);
const totalSigs = (petData.petitions || []).reduce(
(sum: number, p: Petition) => sum + (p.signature_count || 0), 0
);
setStats(prev => ({
...prev,
signatures: totalSigs,
petitions: petData.total || petData.petitions?.length || 0,
}));
}
if (topicRes.ok) {
const topicData = await topicRes.json();
setTopics(topicData.categories || []);
setStats(prev => ({ ...prev, topics: (topicData.categories || []).length }));
}
} catch (err) {
console.error('Failed to load landing page data:', err);
} finally {
setLoading(false);
}
}
load();
}, []);
// Weight petitions by category affinity if user is authenticated
const sortedPetitions = React.useMemo(() => {
if (!session.authenticated || Object.keys(categoryAffinity).length === 0) {
return petitions;
}
// Score each petition by category affinity + featured status
return [...petitions].sort((a, b) => {
const scoreA = (a.is_featured ? 100 : 0) + (categoryAffinity[a.category || 'other'] || 0) * 10;
const scoreB = (b.is_featured ? 100 : 0) + (categoryAffinity[b.category || 'other'] || 0) * 10;
return scoreB - scoreA;
});
}, [petitions, categoryAffinity, session.authenticated]);
// Handle track/untrack
const handleTrack = useCallback(async (petitionId: string, track: boolean) => {
if (!session.authenticated) return;
try {
if (track) {
const res = await fetch('/api/pulse/tracked', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ petition_id: petitionId }),
});
if (res.ok) {
setTrackedIds(prev => new Set(prev).add(petitionId));
}
} else {
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) {
setTrackedIds(prev => { const s = new Set(prev); s.delete(petitionId); return s; });
}
}
} catch {
// Silently fail
}
}, [session.authenticated]);
return (
<div>
{/* ──── Hero Section ──── */}
<section style={{
backgroundColor: C.darkGreen, color: C.textLight, position: 'relative',
overflow: 'hidden',
}}>
{/* Decorative background pattern */}
<div style={{
position: 'absolute', inset: 0, opacity: 0.05,
backgroundImage: 'radial-gradient(circle at 25% 50%, #DAA520 1px, transparent 1px), radial-gradient(circle at 75% 50%, #DAA520 1px, transparent 1px)',
backgroundSize: '60px 60px',
}} />
<div style={{ maxWidth: 1200, margin: '0 auto', padding: '80px 24px 72px', position: 'relative', zIndex: 1 }}>
<div style={{ maxWidth: 720, margin: '0 auto', textAlign: 'center' }}>
{/* Animated POA Badge */}
<div style={{ marginBottom: 20 }}>
<POABadge />
</div>
{/* Rolling Headline */}
<h1 style={{
fontSize: 'clamp(36px, 5vw, 55px)', fontWeight: 'bold', lineHeight: 1.15,
margin: '0 0 20px', fontFamily: 'Georgia, serif',
}}>
<RollingText />.{' '}
<span style={{ opacity: 0.85 }}>Your Petition.</span>
</h1>
{/* Subtitle — personalized if authenticated */}
<p style={{
fontSize: 'clamp(16px, 2vw, 20px)', lineHeight: '30px', opacity: 0.85,
margin: '0 0 36px', fontFamily: 'system-ui, sans-serif',
}}>
{session.authenticated
? `Welcome back, ${session.user}. Continue making a difference through collective action.`
: 'Join thousands making a difference through collective action. Create, sign, and share petitions that drive real change.'}
</p>
{/* CTA buttons */}
<div style={{ display: 'flex', gap: 16, justifyContent: 'center', flexWrap: 'wrap', marginBottom: 48 }}>
<Link
href="/pulse/petitions/create"
style={{
display: 'inline-flex', alignItems: 'center', gap: 8,
backgroundColor: C.gold, color: C.darkGreen, textDecoration: 'none',
fontSize: 16, fontWeight: 700, fontFamily: 'system-ui, sans-serif',
padding: '16px 32px', borderRadius: 9999, transition: 'transform 150ms, background-color 150ms',
}}
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 = C.gold; }}
>
Create A Petition
<ArrowRight size={18} />
</Link>
<Link
href="/pulse/petitions"
style={{
display: 'inline-flex', alignItems: 'center', gap: 8,
backgroundColor: 'transparent', color: C.textLight, textDecoration: 'none',
fontSize: 16, fontWeight: 600, fontFamily: 'system-ui, sans-serif',
padding: '16px 32px', borderRadius: 9999, border: '2px solid rgba(255,255,255,0.4)',
transition: 'border-color 150ms, background-color 150ms',
}}
onMouseEnter={e => { e.currentTarget.style.borderColor = '#fff'; e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.08)'; }}
onMouseLeave={e => { e.currentTarget.style.borderColor = 'rgba(255,255,255,0.4)'; e.currentTarget.style.backgroundColor = 'transparent'; }}
>
Browse Petitions
</Link>
{!session.authenticated && (
<Link
href="/login?returnTo=/pulse"
style={{
display: 'inline-flex', alignItems: 'center', gap: 8,
backgroundColor: 'transparent', color: C.textLight, textDecoration: 'none',
fontSize: 16, fontWeight: 500, fontFamily: 'system-ui, sans-serif',
padding: '16px 24px', borderRadius: 9999,
transition: 'opacity 150ms', opacity: 0.75,
}}
onMouseEnter={e => (e.currentTarget.style.opacity = '1')}
onMouseLeave={e => (e.currentTarget.style.opacity = '0.75')}
>
<LogIn size={18} />
Sign In
</Link>
)}
</div>
{/* Stats row */}
<div style={{
display: 'flex', justifyContent: 'center', gap: 0, flexWrap: 'wrap',
}}>
{[
{ icon: <Users size={20} />, value: stats.signatures > 0 ? `${formatNumber(stats.signatures)}+` : '5,000+', label: 'Signatures' },
{ icon: <FileText size={20} />, value: stats.petitions > 0 ? `${stats.petitions}+` : '50+', label: 'Active Petitions' },
{ icon: <Tag size={20} />, value: stats.topics > 0 ? `${stats.topics}` : '12', label: 'Topics' },
].map((stat, i) => (
<div
key={stat.label}
style={{
padding: '16px 32px', textAlign: 'center',
borderLeft: i > 0 ? '1px solid rgba(255,255,255,0.15)' : 'none',
}}
>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8, marginBottom: 4 }}>
<span style={{ color: C.gold }}>{stat.icon}</span>
<span style={{ fontSize: 24, fontWeight: 'bold', fontFamily: 'Georgia, serif' }}>{stat.value}</span>
</div>
<span style={{ fontSize: 13, opacity: 0.7, fontFamily: 'system-ui, sans-serif' }}>{stat.label}</span>
</div>
))}
</div>
</div>
</div>
{/* Brand Marquee */}
<BrandMarquee />
{/* Wave divider */}
<div style={{ position: 'relative', height: 40, overflow: 'hidden' }}>
<svg viewBox="0 0 1200 40" preserveAspectRatio="none" style={{ position: 'absolute', bottom: 0, width: '100%', height: 40 }}>
<path d="M0,20 Q300,40 600,20 T1200,20 L1200,40 L0,40 Z" fill={C.cream} />
</svg>
</div>
</section>
{/* ──── Quick Links (authenticated only) ──── */}
<QuickLinksBar session={session} />
{/* ──── Featured Petitions ──── */}
<section style={{ maxWidth: 1200, margin: '0 auto', padding: '48px 24px 64px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 32, flexWrap: 'wrap', gap: 12 }}>
<div>
<h2 style={{ fontSize: 36, fontWeight: 'bold', color: C.text, margin: 0, fontFamily: 'Georgia, serif' }}>
{session.authenticated && Object.keys(categoryAffinity).length > 0
? 'Recommended For You'
: 'Featured Petitions'}
</h2>
<p style={{ fontSize: 16, color: C.textMuted, margin: '8px 0 0', fontFamily: 'system-ui, sans-serif' }}>
{session.authenticated && Object.keys(categoryAffinity).length > 0
? 'Petitions matching your interests and activity'
: 'Active campaigns making a difference right now'}
</p>
</div>
<Link
href="/pulse/petitions"
style={{
display: 'inline-flex', alignItems: 'center', gap: 6,
color: C.darkGreen, textDecoration: 'none', fontSize: 15, fontWeight: 600,
fontFamily: 'system-ui, sans-serif',
}}
>
View all petitions
<ChevronRight size={18} />
</Link>
</div>
{loading ? (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(340px, 1fr))', gap: 24 }}>
{[1, 2, 3].map(i => (
<div key={i} style={{ backgroundColor: '#fff', borderRadius: 16, height: 280, border: '1px solid #e5e7eb' }}>
<div style={{ padding: 24 }}>
<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: 20, backgroundColor: '#e5e7eb', borderRadius: 7, width: '70%', marginBottom: 16 }} />
<div style={{ height: 14, backgroundColor: '#e5e7eb', borderRadius: 7, width: '100%', marginBottom: 8 }} />
<div style={{ height: 14, backgroundColor: '#e5e7eb', borderRadius: 7, width: '80%' }} />
</div>
</div>
))}
</div>
) : sortedPetitions.length === 0 ? (
<div style={{ textAlign: 'center', padding: 60, color: C.textMuted, fontFamily: 'system-ui, sans-serif' }}>
<FileText size={48} style={{ opacity: 0.3, marginBottom: 16 }} />
<p style={{ fontSize: 18 }}>No active petitions yet. Be the first to create one!</p>
</div>
) : (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(340px, 1fr))', gap: 24 }}>
{sortedPetitions.slice(0, 6).map(p => (
<PetitionCard
key={p.id}
petition={p}
isTracked={trackedIds.has(p.id)}
onTrack={handleTrack}
isAuthenticated={session.authenticated}
/>
))}
</div>
)}
{/* Show more if there are more than 6 */}
{!loading && sortedPetitions.length > 6 && (
<div style={{ textAlign: 'center', marginTop: 32 }}>
<Link
href="/pulse/petitions"
style={{
display: 'inline-flex', alignItems: 'center', gap: 8,
backgroundColor: '#fff', color: C.darkGreen, textDecoration: 'none',
fontSize: 15, fontWeight: 600, fontFamily: 'system-ui, sans-serif',
padding: '12px 28px', borderRadius: 9999, border: '2px solid ' + C.darkGreen,
transition: 'all 150ms',
}}
onMouseEnter={e => { e.currentTarget.style.backgroundColor = C.darkGreen; e.currentTarget.style.color = '#fff'; }}
onMouseLeave={e => { e.currentTarget.style.backgroundColor = '#fff'; e.currentTarget.style.color = C.darkGreen; }}
>
View All {stats.petitions}+ Petitions
<ArrowRight size={16} />
</Link>
</div>
)}
</section>
{/* ──── Topics Section ──── */}
{topics.length > 0 && (
<section style={{ backgroundColor: '#fff', borderTop: '1px solid #e5e7eb', borderBottom: '1px solid #e5e7eb' }}>
<div style={{ maxWidth: 1200, margin: '0 auto', padding: '64px 24px' }}>
<div style={{ textAlign: 'center', marginBottom: 36 }}>
<h2 style={{ fontSize: 36, fontWeight: 'bold', color: C.text, margin: 0, fontFamily: 'Georgia, serif' }}>
Browse by Topic
</h2>
<p style={{ fontSize: 16, color: C.textMuted, margin: '8px 0 0', fontFamily: 'system-ui, sans-serif' }}>
Find petitions that matter to you
</p>
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'center', gap: 12 }}>
{topics.slice(0, 12).map(t => {
const hasAffinity = categoryAffinity[t.category] > 0;
return (
<Link
key={t.category}
href={`/pulse/petitions?category=${encodeURIComponent(t.category)}`}
style={{
display: 'inline-flex', alignItems: 'center', gap: 8,
padding: '10px 20px', borderRadius: 9999,
backgroundColor: hasAffinity ? 'rgba(218,165,32,0.08)' : C.cream,
border: `1px solid ${hasAffinity ? C.gold : C.border}`,
color: C.text, textDecoration: 'none',
fontSize: 14, fontWeight: hasAffinity ? 600 : 500,
fontFamily: 'system-ui, sans-serif',
transition: 'all 150ms',
}}
onMouseEnter={e => { e.currentTarget.style.backgroundColor = C.darkGreen; e.currentTarget.style.color = '#fff'; e.currentTarget.style.borderColor = C.darkGreen; }}
onMouseLeave={e => { e.currentTarget.style.backgroundColor = hasAffinity ? 'rgba(218,165,32,0.08)' : C.cream; e.currentTarget.style.color = C.text; e.currentTarget.style.borderColor = hasAffinity ? C.gold : C.border; }}
>
<Tag size={14} style={{ opacity: 0.6 }} />
{categoryLabel(t.category)}
<span style={{ fontSize: 12, opacity: 0.6 }}>({t.count})</span>
</Link>
);
})}
</div>
</div>
</section>
)}
{/* ──── CTA Banner ──── */}
<section style={{ backgroundColor: C.darkGreen, position: 'relative', overflow: 'hidden' }}>
<div style={{
position: 'absolute', inset: 0, opacity: 0.05,
backgroundImage: 'radial-gradient(circle, #DAA520 1px, transparent 1px)',
backgroundSize: '40px 40px',
}} />
<div style={{ maxWidth: 800, margin: '0 auto', padding: '72px 24px', textAlign: 'center', position: 'relative', zIndex: 1 }}>
<h2 style={{ fontSize: 'clamp(28px, 4vw, 42px)', fontWeight: 'bold', color: C.textLight, margin: '0 0 16px', fontFamily: 'Georgia, serif' }}>
Ready to Make a <span style={{ color: C.gold }}>Difference</span>?
</h2>
<p style={{ fontSize: 18, color: 'rgba(255,255,255,0.8)', margin: '0 0 32px', fontFamily: 'system-ui, sans-serif', lineHeight: '28px' }}>
Every petition starts with one voice. Add yours today and help shape the future.
</p>
<Link
href="/pulse/petitions/create"
style={{
display: 'inline-flex', alignItems: 'center', gap: 8,
backgroundColor: C.gold, color: C.darkGreen, textDecoration: 'none',
fontSize: 18, fontWeight: 700, fontFamily: 'system-ui, sans-serif',
padding: '18px 40px', borderRadius: 9999, transition: 'transform 150ms, background-color 150ms',
}}
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 = C.gold; }}
>
Start Your Petition
<TrendingUp size={20} />
</Link>
</div>
</section>
</div>
);
}