d74b7523d3
- Contact form: notifyOnAnswer checkbox (shown for Bible Questions only) - Contact route: stores notifyOnAnswer flag on question record - Email: buildQuestionAnsweredEmailTemplate for branded notification - Questions route: sends notification email on approve when notifyOnAnswer + email + answer are set - Q&A section: Canvas API "Save as image" button generates 1080x1080 PNG quote card Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
164 lines
5.4 KiB
TypeScript
164 lines
5.4 KiB
TypeScript
import { useState, useEffect } from 'react'
|
|
import type { ChangeEvent } from 'react'
|
|
import { useNavigate, useSearchParams } from 'react-router-dom'
|
|
|
|
const EMAIL_REGEX = /^[a-zA-Z0-9._%+\-]{1,64}@[a-zA-Z0-9.\-]{1,253}\.[a-zA-Z]{2,}$/
|
|
|
|
interface ContactFields {
|
|
firstName: string
|
|
lastName: string
|
|
email: string
|
|
message: string
|
|
messageType: string
|
|
}
|
|
|
|
export default function ContactForm() {
|
|
const navigate = useNavigate()
|
|
const [searchParams] = useSearchParams()
|
|
const source = searchParams.get('source')
|
|
const studyName = searchParams.get('study')
|
|
|
|
const [fields, setFields] = useState<ContactFields>({
|
|
firstName: '',
|
|
lastName: '',
|
|
email: '',
|
|
message: '',
|
|
messageType: source === 'community' ? 'community' : 'question',
|
|
})
|
|
|
|
useEffect(() => {
|
|
if (source === 'community' && studyName) {
|
|
setFields(f => ({
|
|
...f,
|
|
messageType: 'community',
|
|
message: f.message.startsWith('[From ') ? f.message : `[From ${decodeURIComponent(studyName)} community]\n\n${f.message}`,
|
|
}))
|
|
}
|
|
}, [source, studyName])
|
|
const [subscribe, setSubscribe] = useState(true)
|
|
const [notifyOnAnswer, setNotifyOnAnswer] = useState(true)
|
|
const [honey, setHoney] = useState('')
|
|
const [status, setStatus] = useState<'idle' | 'submitting' | 'error'>('idle')
|
|
const [errorMsg, setErrorMsg] = useState('')
|
|
const [fieldErrors, setFieldErrors] = useState<{ email?: string }>({})
|
|
|
|
function handleChange(e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) {
|
|
const { name, value } = e.target
|
|
setFields(f => ({ ...f, [name]: value }))
|
|
if (name === 'email' && fieldErrors.email) {
|
|
setFieldErrors(prev => ({ ...prev, email: undefined }))
|
|
}
|
|
}
|
|
|
|
function handleEmailBlur() {
|
|
if (!fields.email) return
|
|
if (!EMAIL_REGEX.test(fields.email)) {
|
|
setFieldErrors(prev => ({ ...prev, email: 'Please enter a valid email address.' }))
|
|
} else {
|
|
setFieldErrors(prev => ({ ...prev, email: undefined }))
|
|
}
|
|
}
|
|
|
|
function handleSelectChange(e: ChangeEvent<HTMLSelectElement>) {
|
|
setFields(f => ({ ...f, [e.target.name]: e.target.value }))
|
|
}
|
|
|
|
async function handleSubmit(e: React.FormEvent) {
|
|
e.preventDefault()
|
|
setStatus('submitting')
|
|
setErrorMsg('')
|
|
|
|
try {
|
|
const res = await fetch('/api/contact', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ ...fields, subscribe, notifyOnAnswer: fields.messageType === 'question' ? notifyOnAnswer : false, _honey: honey }),
|
|
})
|
|
|
|
if (!res.ok) {
|
|
const data = await res.json().catch(() => ({}))
|
|
setErrorMsg((data as { message?: string }).message ?? 'Something went wrong. Please try again.')
|
|
setStatus('error')
|
|
return
|
|
}
|
|
|
|
navigate('/thanks')
|
|
} catch {
|
|
setErrorMsg('Could not connect. Please try again later.')
|
|
setStatus('error')
|
|
}
|
|
}
|
|
|
|
return (
|
|
<form className="contact-form" onSubmit={handleSubmit} noValidate>
|
|
<input
|
|
type="text"
|
|
className="contact-honeypot"
|
|
tabIndex={-1}
|
|
autoComplete="off"
|
|
aria-hidden="true"
|
|
value={honey}
|
|
onChange={e => setHoney(e.target.value)}
|
|
/>
|
|
<label>
|
|
First Name
|
|
<input type="text" name="firstName" required autoComplete="given-name" value={fields.firstName} onChange={handleChange} />
|
|
</label>
|
|
<label>
|
|
Last Name
|
|
<input type="text" name="lastName" required autoComplete="family-name" value={fields.lastName} onChange={handleChange} />
|
|
</label>
|
|
<label>
|
|
Email
|
|
<input
|
|
type="email"
|
|
name="email"
|
|
required
|
|
autoComplete="email"
|
|
value={fields.email}
|
|
onChange={handleChange}
|
|
onBlur={handleEmailBlur}
|
|
aria-describedby={fieldErrors.email ? 'contact-email-error' : undefined}
|
|
/>
|
|
{fieldErrors.email && <span id="contact-email-error" className="study-signup-field-error">{fieldErrors.email}</span>}
|
|
</label>
|
|
<label>
|
|
Message Type
|
|
<select name="messageType" value={fields.messageType} onChange={handleSelectChange}>
|
|
<option value="question">Bible Question</option>
|
|
<option value="community">Community Question</option>
|
|
<option value="testimony">Testimony</option>
|
|
<option value="topic">Topic Request</option>
|
|
<option value="general">General Message</option>
|
|
</select>
|
|
</label>
|
|
<label>
|
|
Message
|
|
<textarea name="message" rows={6} required value={fields.message} onChange={handleChange} />
|
|
</label>
|
|
{fields.messageType === 'question' && (
|
|
<label className="contact-consent">
|
|
<input
|
|
type="checkbox"
|
|
checked={notifyOnAnswer}
|
|
onChange={e => setNotifyOnAnswer(e.target.checked)}
|
|
/>
|
|
<span>Email me when this question is answered.</span>
|
|
</label>
|
|
)}
|
|
<label className="contact-consent">
|
|
<input
|
|
type="checkbox"
|
|
checked={subscribe}
|
|
onChange={e => setSubscribe(e.target.checked)}
|
|
/>
|
|
<span>Send me updates from Verse by Verse with Nate. I can unsubscribe anytime.</span>
|
|
</label>
|
|
{status === 'error' && <p className="contact-error">{errorMsg}</p>}
|
|
<button type="submit" className="btn-primary" disabled={status === 'submitting'}>
|
|
{status === 'submitting' ? 'Sending…' : 'Send Message'}
|
|
</button>
|
|
</form>
|
|
)
|
|
}
|