Contact page redesign: message-type chips with hints, side-by-side names, character count, sticky profile card, form first on phones, compact Ko-fi card; v2.27.0
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d06fcba60b
commit
3cf7e02561
5 files changed
+254
-253
No files matched your search
@@ -3,6 +3,7 @@ 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,}$/
|
||||
const MAX_MESSAGE = 3000
|
||||
|
||||
interface ContactFields {
|
||||
firstName: string
|
||||
@@ -12,12 +13,20 @@ interface ContactFields {
|
||||
messageType: string
|
||||
}
|
||||
|
||||
const TYPES: { value: string; label: string; hint: string; placeholder: string }[] = [
|
||||
{ value: 'question', label: 'Bible question', hint: 'Ask anything about Scripture. Answers are posted on the Q&A page.', placeholder: 'What would you like to know?' },
|
||||
{ value: 'community', label: 'Community question', hint: 'A question about a study discussion or the community.', placeholder: 'What’s on your mind?' },
|
||||
{ value: 'testimony', label: 'Testimony', hint: 'Share what God is doing in your life. Every one is read.', placeholder: 'Tell your story…' },
|
||||
{ value: 'topic', label: 'Topic request', hint: 'Suggest a passage or topic for a future episode.', placeholder: 'Which passage or topic?' },
|
||||
{ value: 'general', label: 'General message', hint: 'Anything else you’d like to say.', placeholder: 'Write your message…' },
|
||||
]
|
||||
|
||||
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: '',
|
||||
@@ -42,6 +51,8 @@ export default function ContactForm() {
|
||||
const [errorMsg, setErrorMsg] = useState('')
|
||||
const [fieldErrors, setFieldErrors] = useState<{ email?: string }>({})
|
||||
|
||||
const activeType = TYPES.find(t => t.value === fields.messageType) ?? TYPES[0]
|
||||
|
||||
function handleChange(e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) {
|
||||
const { name, value } = e.target
|
||||
setFields(f => ({ ...f, [name]: value }))
|
||||
@@ -59,10 +70,6 @@ export default function ContactForm() {
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelectChange(e: ChangeEvent<HTMLSelectElement>) {
|
||||
setFields(f => ({ ...f, [e.target.name]: e.target.value }))
|
||||
}
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
setStatus('submitting')
|
||||
@@ -90,7 +97,7 @@ export default function ContactForm() {
|
||||
}
|
||||
|
||||
return (
|
||||
<form className="contact-form" onSubmit={handleSubmit} noValidate>
|
||||
<form className="cx-form" onSubmit={handleSubmit} noValidate>
|
||||
<input
|
||||
type="text"
|
||||
className="contact-honeypot"
|
||||
@@ -100,14 +107,37 @@ export default function ContactForm() {
|
||||
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>
|
||||
|
||||
<fieldset className="cx-types">
|
||||
<legend>What is this about?</legend>
|
||||
<div className="cx-type-row" role="radiogroup">
|
||||
{TYPES.map(t => (
|
||||
<label key={t.value} className="cx-type">
|
||||
<input
|
||||
type="radio"
|
||||
name="messageType"
|
||||
value={t.value}
|
||||
checked={fields.messageType === t.value}
|
||||
onChange={() => setFields(f => ({ ...f, messageType: t.value }))}
|
||||
/>
|
||||
<span>{t.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<p className="cx-hint" aria-live="polite">{activeType.hint}</p>
|
||||
</fieldset>
|
||||
|
||||
<div className="cx-row">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<label>
|
||||
Email
|
||||
<input
|
||||
@@ -122,41 +152,37 @@ export default function ContactForm() {
|
||||
/>
|
||||
{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)}
|
||||
<textarea
|
||||
name="message"
|
||||
rows={6}
|
||||
required
|
||||
maxLength={MAX_MESSAGE}
|
||||
placeholder={activeType.placeholder}
|
||||
value={fields.message}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<span>Send me updates from Verse by Verse with Nate. I can unsubscribe anytime.</span>
|
||||
<span className="cx-count">{fields.message.length} / {MAX_MESSAGE}</span>
|
||||
</label>
|
||||
{status === 'error' && <p className="contact-error">{errorMsg}</p>}
|
||||
<button type="submit" className="btn-primary" disabled={status === 'submitting'}>
|
||||
{status === 'submitting' ? 'Sending…' : 'Send Message'}
|
||||
|
||||
<div className="cx-checks">
|
||||
{fields.messageType === 'question' && (
|
||||
<label className="cx-check">
|
||||
<input type="checkbox" checked={notifyOnAnswer} onChange={e => setNotifyOnAnswer(e.target.checked)} />
|
||||
<span>Email me when this question is answered.</span>
|
||||
</label>
|
||||
)}
|
||||
<label className="cx-check">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{status === 'error' && <p className="contact-error" role="alert">{errorMsg}</p>}
|
||||
<button type="submit" className="btn-primary cx-submit" disabled={status === 'submitting'}>
|
||||
{status === 'submitting' ? 'Sending…' : 'Send message'}
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user