← back to Norma
app/pulse/petitions/create/page.tsx
440 lines
'use client';
import React, { useRef, useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import {
ArrowLeft, Send, Loader2, CheckCircle2, Target, Tag,
FileText, Users, AlertCircle,
} from 'lucide-react';
/* ────────────────────────── Colors ────────────────────────── */
const C = {
darkGreen: '#1B4332',
gold: '#DAA520',
cream: '#FAFAF5',
text: '#1a1a1a',
textLight: '#fafafa',
textMuted: '#6b7280',
border: '#d1d5db',
borderLight: '#e5e7eb',
success: '#059669',
error: '#dc2626',
};
/* ────────────────────────── Categories ────────────────────────── */
const CATEGORIES = [
{ value: 'debt_cancellation', label: 'Debt Cancellation' },
{ value: 'consumer_protection', label: 'Consumer Protection' },
{ value: 'voting_rights', label: 'Voting Rights' },
{ value: 'legal', label: 'Legal' },
{ value: 'education', label: 'Education' },
{ value: 'environment', label: 'Environment' },
{ value: 'healthcare', label: 'Healthcare' },
{ value: 'other', label: 'Other' },
];
const GOAL_OPTIONS = [
{ value: 1000, label: '1,000' },
{ value: 5000, label: '5,000' },
{ value: 10000, label: '10,000' },
{ value: 25000, label: '25,000' },
{ value: 50000, label: '50,000' },
{ value: 100000, label: '100,000' },
];
/* ────────────────────────── Component ────────────────────────── */
export default function CreatePetitionPage() {
const router = useRouter();
const [title, setTitle] = useState('');
const [body, setBody] = useState('');
const [target, setTarget] = useState('');
const [category, setCategory] = useState('other');
const [tagsInput, setTagsInput] = useState('');
const [signatureGoal, setSignatureGoal] = useState(10000);
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<{ id: string; title: string } | null>(null);
const inFlight = useRef(false);
const canSubmit = title.trim().length >= 5 && !submitting;
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (inFlight.current || !canSubmit) return;
inFlight.current = true;
setSubmitting(true);
setError(null);
try {
const res = await fetch('/api/pulse/petitions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: title.trim(),
body: body.trim() || undefined,
target: target.trim() || undefined,
category,
tags: tagsInput.trim() || undefined,
signature_goal: signatureGoal,
}),
});
const data = await res.json();
if (!res.ok) {
setError(data.error || 'Something went wrong. Please try again.');
return;
}
setSuccess({ id: data.petition.id, title: data.petition.title });
} catch (err) {
setError('Network error. Please check your connection and try again.');
} finally {
setSubmitting(false);
inFlight.current = false;
}
}
/* ── Success state ── */
if (success) {
return (
<div style={{ maxWidth: 640, margin: '0 auto', padding: '48px 24px 80px' }}>
<div style={{
backgroundColor: '#fff', borderRadius: 20, padding: '48px 32px',
textAlign: 'center', border: `2px solid ${C.success}`,
boxShadow: '0 4px 20px rgba(5,150,105,0.1)',
}}>
<CheckCircle2 size={56} style={{ color: C.success, marginBottom: 20 }} />
<h1 style={{ fontSize: 28, fontWeight: 'bold', color: C.text, margin: '0 0 12px', fontFamily: 'Georgia, serif' }}>
Petition Created!
</h1>
<p style={{ fontSize: 16, color: C.textMuted, margin: '0 0 8px', fontFamily: 'system-ui, sans-serif', lineHeight: '24px' }}>
Your petition <strong style={{ color: C.text }}>“{success.title}”</strong> is now live.
</p>
<p style={{ fontSize: 14, color: C.textMuted, margin: '0 0 32px', fontFamily: 'system-ui, sans-serif' }}>
Share it to start collecting signatures.
</p>
<div style={{ display: 'flex', gap: 12, justifyContent: 'center', flexWrap: 'wrap' }}>
<Link
href={`/pulse/petition/${success.id}`}
style={{
display: 'inline-flex', alignItems: 'center', gap: 8,
backgroundColor: C.darkGreen, color: C.textLight, textDecoration: 'none',
fontSize: 15, fontWeight: 600, fontFamily: 'system-ui, sans-serif',
padding: '14px 28px', borderRadius: 9999,
}}
>
View Petition
</Link>
<Link
href="/pulse/petitions"
style={{
display: 'inline-flex', alignItems: 'center', gap: 8,
backgroundColor: 'transparent', color: C.darkGreen, textDecoration: 'none',
fontSize: 15, fontWeight: 600, fontFamily: 'system-ui, sans-serif',
padding: '14px 28px', borderRadius: 9999,
border: `1px solid ${C.border}`,
}}
>
Browse Petitions
</Link>
</div>
</div>
</div>
);
}
/* ── Form ── */
return (
<div style={{ maxWidth: 720, margin: '0 auto', padding: '32px 24px 80px' }}>
{/* Back link */}
<Link
href="/pulse/petitions"
style={{
display: 'inline-flex', alignItems: 'center', gap: 6,
color: C.darkGreen, textDecoration: 'none', fontSize: 14, fontWeight: 500,
fontFamily: 'system-ui, sans-serif', marginBottom: 24,
}}
>
<ArrowLeft size={16} />
Back to Petitions
</Link>
{/* Header */}
<div style={{ marginBottom: 32 }}>
<h1 style={{
fontSize: 'clamp(28px, 4vw, 40px)', fontWeight: 'bold', color: C.text,
margin: '0 0 8px', fontFamily: 'Georgia, serif',
}}>
Create a Petition
</h1>
<p style={{ fontSize: 16, color: C.textMuted, margin: 0, fontFamily: 'system-ui, sans-serif', lineHeight: '24px' }}>
Start a petition to make your voice heard. Fill in the details below and we will publish it for others to sign.
</p>
</div>
{/* Form card */}
<form onSubmit={handleSubmit}>
<div style={{
backgroundColor: '#fff', borderRadius: 20, padding: '32px 28px',
border: `1px solid ${C.borderLight}`,
boxShadow: '0 1px 3px rgba(0,0,0,0.06)',
}}>
{/* Error banner */}
{error && (
<div style={{
display: 'flex', alignItems: 'center', gap: 10,
backgroundColor: 'rgba(220,38,38,0.06)', border: `1px solid rgba(220,38,38,0.2)`,
borderRadius: 12, padding: '12px 16px', marginBottom: 24,
}}>
<AlertCircle size={18} style={{ color: C.error, flexShrink: 0 }} />
<span style={{ fontSize: 14, color: C.error, fontFamily: 'system-ui, sans-serif' }}>{error}</span>
</div>
)}
{/* Title */}
<div style={{ marginBottom: 24 }}>
<label
htmlFor="petition-title"
style={{
display: 'flex', alignItems: 'center', gap: 6, fontSize: 14, fontWeight: 600,
color: C.text, marginBottom: 8, fontFamily: 'system-ui, sans-serif',
}}
>
<FileText size={15} style={{ color: C.darkGreen }} />
Petition Title <span style={{ color: C.error }}>*</span>
</label>
<input
id="petition-title"
type="text"
placeholder="e.g., Expand Access to Education for Public Service Workers"
value={title}
onChange={e => setTitle(e.target.value)}
maxLength={300}
required
style={{
width: '100%', padding: '14px 16px', borderRadius: 12,
border: `1px solid ${C.border}`, backgroundColor: C.cream,
fontSize: 15, fontFamily: 'system-ui, sans-serif', color: C.text,
outline: 'none', boxSizing: 'border-box',
}}
onFocus={e => (e.target.style.borderColor = C.darkGreen)}
onBlur={e => (e.target.style.borderColor = C.border)}
/>
<div style={{
display: 'flex', justifyContent: 'space-between', marginTop: 6,
fontSize: 12, color: C.textMuted, fontFamily: 'system-ui, sans-serif',
}}>
<span>{title.length < 5 && title.length > 0 ? 'At least 5 characters required' : ''}</span>
<span>{title.length}/300</span>
</div>
</div>
{/* Body */}
<div style={{ marginBottom: 24 }}>
<label
htmlFor="petition-body"
style={{
display: 'flex', alignItems: 'center', gap: 6, fontSize: 14, fontWeight: 600,
color: C.text, marginBottom: 8, fontFamily: 'system-ui, sans-serif',
}}
>
Description
</label>
<textarea
id="petition-body"
placeholder="Describe why this petition matters and what change you want to see..."
value={body}
onChange={e => setBody(e.target.value)}
rows={6}
style={{
width: '100%', padding: '14px 16px', borderRadius: 12,
border: `1px solid ${C.border}`, backgroundColor: C.cream,
fontSize: 15, fontFamily: 'system-ui, sans-serif', color: C.text,
outline: 'none', boxSizing: 'border-box', resize: 'vertical',
lineHeight: '22px',
}}
onFocus={e => (e.target.style.borderColor = C.darkGreen)}
onBlur={e => (e.target.style.borderColor = C.border)}
/>
</div>
{/* Target */}
<div style={{ marginBottom: 24 }}>
<label
htmlFor="petition-target"
style={{
display: 'flex', alignItems: 'center', gap: 6, fontSize: 14, fontWeight: 600,
color: C.text, marginBottom: 8, fontFamily: 'system-ui, sans-serif',
}}
>
<Target size={15} style={{ color: C.darkGreen }} />
Target (Who should act?)
</label>
<input
id="petition-target"
type="text"
placeholder="e.g., U.S. Department of Education, Congress, Governor"
value={target}
onChange={e => setTarget(e.target.value)}
maxLength={200}
style={{
width: '100%', padding: '14px 16px', borderRadius: 12,
border: `1px solid ${C.border}`, backgroundColor: C.cream,
fontSize: 15, fontFamily: 'system-ui, sans-serif', color: C.text,
outline: 'none', boxSizing: 'border-box',
}}
onFocus={e => (e.target.style.borderColor = C.darkGreen)}
onBlur={e => (e.target.style.borderColor = C.border)}
/>
</div>
{/* Category */}
<div style={{ marginBottom: 24 }}>
<label
htmlFor="petition-category"
style={{
display: 'flex', alignItems: 'center', gap: 6, fontSize: 14, fontWeight: 600,
color: C.text, marginBottom: 8, fontFamily: 'system-ui, sans-serif',
}}
>
Category
</label>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
{CATEGORIES.map(cat => (
<button
key={cat.value}
type="button"
onClick={() => setCategory(cat.value)}
style={{
padding: '8px 18px', borderRadius: 9999, fontSize: 13,
fontFamily: 'system-ui, sans-serif', fontWeight: 500, cursor: 'pointer',
border: category === cat.value ? `2px solid ${C.darkGreen}` : `1px solid ${C.border}`,
backgroundColor: category === cat.value ? `${C.darkGreen}0a` : 'transparent',
color: category === cat.value ? C.darkGreen : C.text,
transition: 'all 150ms',
}}
>
{cat.label}
</button>
))}
</div>
</div>
{/* Tags */}
<div style={{ marginBottom: 24 }}>
<label
htmlFor="petition-tags"
style={{
display: 'flex', alignItems: 'center', gap: 6, fontSize: 14, fontWeight: 600,
color: C.text, marginBottom: 8, fontFamily: 'system-ui, sans-serif',
}}
>
<Tag size={15} style={{ color: C.darkGreen }} />
Tags
</label>
<input
id="petition-tags"
type="text"
placeholder="e.g., student debt, education, relief (comma-separated)"
value={tagsInput}
onChange={e => setTagsInput(e.target.value)}
maxLength={200}
style={{
width: '100%', padding: '14px 16px', borderRadius: 12,
border: `1px solid ${C.border}`, backgroundColor: C.cream,
fontSize: 15, fontFamily: 'system-ui, sans-serif', color: C.text,
outline: 'none', boxSizing: 'border-box',
}}
onFocus={e => (e.target.style.borderColor = C.darkGreen)}
onBlur={e => (e.target.style.borderColor = C.border)}
/>
<p style={{ fontSize: 12, color: C.textMuted, margin: '6px 0 0', fontFamily: 'system-ui, sans-serif' }}>
Separate tags with commas. Up to 10 tags.
</p>
</div>
{/* Signature Goal */}
<div style={{ marginBottom: 32 }}>
<label
style={{
display: 'flex', alignItems: 'center', gap: 6, fontSize: 14, fontWeight: 600,
color: C.text, marginBottom: 8, fontFamily: 'system-ui, sans-serif',
}}
>
<Users size={15} style={{ color: C.darkGreen }} />
Signature Goal
</label>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
{GOAL_OPTIONS.map(opt => (
<button
key={opt.value}
type="button"
onClick={() => setSignatureGoal(opt.value)}
style={{
padding: '8px 18px', borderRadius: 9999, fontSize: 13,
fontFamily: 'system-ui, sans-serif', fontWeight: 500, cursor: 'pointer',
border: signatureGoal === opt.value ? `2px solid ${C.gold}` : `1px solid ${C.border}`,
backgroundColor: signatureGoal === opt.value ? `${C.gold}15` : 'transparent',
color: signatureGoal === opt.value ? C.darkGreen : C.text,
transition: 'all 150ms',
}}
>
{opt.label}
</button>
))}
</div>
</div>
{/* Divider */}
<div style={{ borderTop: `1px solid ${C.borderLight}`, marginBottom: 24 }} />
{/* Submit */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 12 }}>
<p style={{ fontSize: 13, color: C.textMuted, margin: 0, fontFamily: 'system-ui, sans-serif' }}>
<span style={{ color: C.error }}>*</span> Required field
</p>
<button
type="submit"
disabled={!canSubmit}
style={{
display: 'inline-flex', alignItems: 'center', gap: 8,
backgroundColor: canSubmit ? C.gold : C.borderLight,
color: canSubmit ? C.darkGreen : C.textMuted,
border: 'none', borderRadius: 9999, padding: '14px 32px',
fontSize: 16, fontWeight: 700, fontFamily: 'system-ui, sans-serif',
cursor: canSubmit ? 'pointer' : 'not-allowed',
transition: 'all 150ms',
}}
onMouseEnter={e => { if (canSubmit) e.currentTarget.style.backgroundColor = '#c8941a'; }}
onMouseLeave={e => { if (canSubmit) e.currentTarget.style.backgroundColor = C.gold; }}
>
{submitting ? (
<>
<Loader2 size={18} style={{ animation: 'spin 1s linear infinite' }} />
Submitting...
</>
) : (
<>
<Send size={18} />
Publish Petition
</>
)}
</button>
</div>
</div>
</form>
<style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style>
</div>
);
}