← back to Patty
app/petitions/[slug]/SignatureForm.tsx
286 lines
'use client';
import { useState, FormEvent } from 'react';
interface SignatureFormProps {
petitionId: string;
petitionTitle: string;
isActive: boolean;
}
export default function SignatureForm({ petitionId, petitionTitle, isActive }: SignatureFormProps) {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [zip, setZip] = useState('');
const [comment, setComment] = useState('');
const [optedIn, setOptedIn] = useState(false);
const [honeypot, setHoneypot] = useState('');
const [submitting, setSubmitting] = useState(false);
const [result, setResult] = useState<{ success: boolean; message: string; signatureCount?: number } | null>(null);
if (!isActive) {
return (
<div style={{
backgroundColor: 'var(--color-surface)',
border: '1px solid var(--color-border)',
borderRadius: 'var(--radius-lg)',
padding: '2rem',
textAlign: 'center',
}}>
<div style={{ fontSize: '2rem', marginBottom: '0.75rem' }}>
{/* Lock icon via SVG */}
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="var(--color-text-muted)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ display: 'inline-block' }}>
<rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
</svg>
</div>
<h3 style={{ color: 'var(--color-text)', fontSize: '1.125rem', fontWeight: 600, marginBottom: '0.5rem' }}>
This petition is not currently accepting signatures
</h3>
<p style={{ color: 'var(--color-text-muted)', fontSize: '0.875rem' }}>
The petition may be under review, paused, or has been closed. Check back later.
</p>
</div>
);
}
async function handleSubmit(e: FormEvent) {
e.preventDefault();
if (honeypot) return; // bot detected
if (!name.trim()) {
setResult({ success: false, message: 'Please enter your name.' });
return;
}
if (!email.trim() || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
setResult({ success: false, message: 'Please enter a valid email address.' });
return;
}
setSubmitting(true);
setResult(null);
try {
const res = await fetch('/api/public/signatures', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
petition_id: petitionId,
signer_name: name.trim(),
signer_email: email.trim().toLowerCase(),
signer_zip: zip.trim() || null,
signer_comment: comment.trim() || null,
opted_in_email: optedIn,
_hp: honeypot, // honeypot field
}),
});
const data = await res.json();
if (res.ok) {
setResult({
success: true,
message: `Thank you for signing! You are signature #${data.signatureCount}.`,
signatureCount: data.signatureCount,
});
// Reset form
setName('');
setEmail('');
setZip('');
setComment('');
setOptedIn(false);
} else {
setResult({
success: false,
message: data.error || 'Something went wrong. Please try again.',
});
}
} catch {
setResult({
success: false,
message: 'Network error. Please check your connection and try again.',
});
} finally {
setSubmitting(false);
}
}
return (
<div style={{
backgroundColor: 'var(--color-surface)',
border: '1px solid var(--color-border)',
borderRadius: 'var(--radius-lg)',
padding: '1.5rem',
}}>
<h3 style={{
color: 'var(--color-text)',
fontSize: '1.25rem',
fontWeight: 700,
marginBottom: '1.25rem',
}}>
Sign This Petition
</h3>
{result && (
<div style={{
padding: '0.75rem 1rem',
borderRadius: 'var(--radius-md)',
marginBottom: '1rem',
backgroundColor: result.success ? 'rgba(34, 197, 94, 0.15)' : 'rgba(239, 68, 68, 0.15)',
border: `1px solid ${result.success ? 'rgba(34, 197, 94, 0.3)' : 'rgba(239, 68, 68, 0.3)'}`,
color: result.success ? 'var(--color-success)' : 'var(--color-error)',
fontSize: '0.875rem',
}}>
{result.message}
</div>
)}
{result?.success ? (
<div style={{ textAlign: 'center', padding: '1rem 0' }}>
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="var(--color-success)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ display: 'inline-block', marginBottom: '0.75rem' }}>
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
<polyline points="22 4 12 14.01 9 11.01" />
</svg>
<p style={{ color: 'var(--color-text)', fontWeight: 600, fontSize: '1rem', marginBottom: '0.5rem' }}>
Your signature has been recorded!
</p>
<p style={{ color: 'var(--color-text-secondary)', fontSize: '0.875rem' }}>
Share this petition to help it reach its goal.
</p>
</div>
) : (
<form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
{/* Honeypot - hidden from humans */}
<div style={{ position: 'absolute', left: '-9999px', opacity: 0, height: 0, overflow: 'hidden' }} aria-hidden="true">
<label htmlFor="website_url">Website</label>
<input
id="website_url"
name="website_url"
type="text"
tabIndex={-1}
autoComplete="off"
value={honeypot}
onChange={(e) => setHoneypot(e.target.value)}
/>
</div>
<div>
<label htmlFor="signer_name" style={{ display: 'block', color: 'var(--color-text-secondary)', fontSize: '0.875rem', fontWeight: 500, marginBottom: '0.375rem' }}>
Full Name <span style={{ color: 'var(--color-error)' }}>*</span>
</label>
<input
id="signer_name"
type="text"
className="input"
placeholder="Your full name"
value={name}
onChange={(e) => setName(e.target.value)}
required
disabled={submitting}
/>
</div>
<div>
<label htmlFor="signer_email" style={{ display: 'block', color: 'var(--color-text-secondary)', fontSize: '0.875rem', fontWeight: 500, marginBottom: '0.375rem' }}>
Email Address <span style={{ color: 'var(--color-error)' }}>*</span>
</label>
<input
id="signer_email"
type="email"
className="input"
placeholder="your@email.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
disabled={submitting}
/>
<p style={{ color: 'var(--color-text-muted)', fontSize: '0.75rem', marginTop: '0.25rem' }}>
Your email is used for verification only and will not be displayed publicly.
</p>
</div>
<div>
<label htmlFor="signer_zip" style={{ display: 'block', color: 'var(--color-text-secondary)', fontSize: '0.875rem', fontWeight: 500, marginBottom: '0.375rem' }}>
ZIP Code <span style={{ color: 'var(--color-text-muted)', fontWeight: 400 }}>(optional)</span>
</label>
<input
id="signer_zip"
type="text"
className="input"
placeholder="e.g. 90210"
value={zip}
onChange={(e) => setZip(e.target.value)}
maxLength={10}
disabled={submitting}
style={{ maxWidth: '12rem' }}
/>
</div>
<div>
<label htmlFor="signer_comment" style={{ display: 'block', color: 'var(--color-text-secondary)', fontSize: '0.875rem', fontWeight: 500, marginBottom: '0.375rem' }}>
Comment <span style={{ color: 'var(--color-text-muted)', fontWeight: 400 }}>(optional)</span>
</label>
<textarea
id="signer_comment"
className="input"
placeholder="Why are you signing this petition?"
value={comment}
onChange={(e) => setComment(e.target.value)}
rows={3}
maxLength={1000}
disabled={submitting}
style={{ resize: 'vertical', minHeight: '4rem' }}
/>
</div>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '0.5rem' }}>
<input
id="opted_in_email"
type="checkbox"
checked={optedIn}
onChange={(e) => setOptedIn(e.target.checked)}
disabled={submitting}
style={{
marginTop: '0.125rem',
accentColor: 'var(--color-primary)',
width: '1rem',
height: '1rem',
cursor: 'pointer',
}}
/>
<label htmlFor="opted_in_email" style={{ color: 'var(--color-text-secondary)', fontSize: '0.875rem', cursor: 'pointer' }}>
Keep me updated on this petition and related campaigns
</label>
</div>
<button
type="submit"
disabled={submitting}
className="btn btn-primary btn-lg"
style={{
width: '100%',
fontSize: '1rem',
fontWeight: 700,
padding: '0.875rem 1.5rem',
marginTop: '0.25rem',
}}
>
{submitting ? (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '0.5rem' }}>
<span className="spinner" style={{ width: '1rem', height: '1rem', borderWidth: '2px' }} />
Signing...
</span>
) : (
'Sign This Petition'
)}
</button>
<p style={{ color: 'var(--color-text-muted)', fontSize: '0.75rem', textAlign: 'center', lineHeight: 1.4 }}>
By signing, you agree that your name may be publicly listed as a supporter. Your email will never be shared.
</p>
</form>
)}
</div>
);
}