Switch contact form to self-hosted SMTP via nodemailer, remove formsubmit.co

This commit is contained in:
nmemmert
2026-04-09 14:38:28 -04:00
parent e93840c25f
commit 4b01d01133
6 changed files with 194 additions and 20 deletions
+14
View File
@@ -572,6 +572,20 @@
margin-top: 0.25rem;
}
.contact-form .btn-primary:disabled {
opacity: 0.55;
cursor: not-allowed;
transform: none;
}
.contact-error {
font-family: 'Barlow Condensed', sans-serif;
font-size: 0.95rem;
color: #e05c5c;
margin: 0;
padding: 0;
}
.contact-honeypot {
position: absolute;
left: -9999px;
+66 -20
View File
@@ -13,8 +13,6 @@ const YOUTUBE_URL = 'https://www.youtube.com/@blackzebraem5558'
const AMAZON_MUSIC_URL =
'https://music.amazon.com/podcasts/202322bf-db86-4e7d-9a6b-4db7cbccbccf/verse-by-verse-with-nate'
const FACEBOOK_URL = 'https://facebook.com/versebyversewithnate'
const CONTACT_FORM_ACTION = 'https://formsubmit.co/vbvwithnate@outlook.com'
const CONTACT_FORM_NEXT = '/thanks'
function FacebookIcon() {
return (
@@ -104,6 +102,71 @@ function AmazonMusicIcon() {
)
}
function ContactForm() {
const navigate = useNavigate()
const [fields, setFields] = useState({ name: '', email: '', message: '' })
const [honey, setHoney] = useState('')
const [status, setStatus] = useState<'idle' | 'submitting' | 'error'>('idle')
const [errorMsg, setErrorMsg] = useState('')
function handleChange(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) {
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, _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>
Name
<input type="text" name="name" required autoComplete="name" value={fields.name} onChange={handleChange} />
</label>
<label>
Email
<input type="email" name="email" required autoComplete="email" value={fields.email} onChange={handleChange} />
</label>
<label>
Message
<textarea name="message" rows={6} required value={fields.message} onChange={handleChange} />
</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>
)
}
function LandingPage({ content }: { content: SiteContent }) {
return (
<div className="site">
@@ -336,24 +399,7 @@ function LandingPage({ content }: { content: SiteContent }) {
Have a question, testimony, or topic request? Send a message and we will get back to you.
</p>
</div>
<form className="contact-form" action={CONTACT_FORM_ACTION} method="POST">
<input type="hidden" name="_subject" value="New message from Verse by Verse website" />
<input type="hidden" name="_next" value={CONTACT_FORM_NEXT} />
<input type="text" name="_honey" className="contact-honeypot" tabIndex={-1} autoComplete="off" />
<label>
Name
<input type="text" name="name" required autoComplete="name" />
</label>
<label>
Email
<input type="email" name="email" required autoComplete="email" />
</label>
<label>
Message
<textarea name="message" rows={6} required />
</label>
<button type="submit" className="btn-primary">Send Message</button>
</form>
<ContactForm />
</div>
</section>