Files
biblical-guide.com/app/[locale]/contact/page.tsx
Andrei 989f231d5a feat: add captcha verification to contact form
Added math-based captcha system to prevent spam on the contact form:
- Created captcha API endpoint with simple arithmetic questions
- Added captcha UI component with refresh functionality
- Integrated captcha verification into contact form submission
- Relaxed spam filters since captcha provides better protection

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-10 17:53:56 +00:00

383 lines
12 KiB
TypeScript

'use client'
import {
Container,
Card,
CardContent,
Typography,
Box,
Button,
TextField,
Paper,
useTheme,
Alert,
Snackbar,
} from '@mui/material'
import {
Email,
Send,
ContactSupport,
} from '@mui/icons-material'
import { useRouter } from 'next/navigation'
import { useTranslations, useLocale } from 'next-intl'
import { useState, useEffect } from 'react'
import RefreshIcon from '@mui/icons-material/Refresh'
export default function Contact() {
const theme = useTheme()
const router = useRouter()
const t = useTranslations('contact')
const locale = useLocale()
const [formData, setFormData] = useState({
name: '',
email: '',
subject: '',
message: ''
})
const [captcha, setCaptcha] = useState<{
id: string
question: string
answer: string
}>({ id: '', question: '', answer: '' })
const [captchaError, setCaptchaError] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false)
const [showSuccess, setShowSuccess] = useState(false)
const [showError, setShowError] = useState(false)
const loadCaptcha = async () => {
try {
const response = await fetch('/api/captcha')
const data = await response.json()
if (data.success) {
setCaptcha({
id: data.captchaId,
question: data.question,
answer: ''
})
setCaptchaError(false)
}
} catch (error) {
console.error('Failed to load captcha:', error)
}
}
useEffect(() => {
loadCaptcha()
}, [])
const handleInputChange = (field: string) => (event: React.ChangeEvent<HTMLInputElement>) => {
setFormData(prev => ({
...prev,
[field]: event.target.value
}))
}
const handleCaptchaChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setCaptcha(prev => ({
...prev,
answer: event.target.value
}))
setCaptchaError(false)
}
const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault()
// Validate captcha answer is provided
if (!captcha.answer.trim()) {
setCaptchaError(true)
return
}
setIsSubmitting(true)
try {
const response = await fetch('/api/contact', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...formData,
captchaId: captcha.id,
captchaAnswer: captcha.answer
})
})
const data = await response.json()
if (response.ok && data.success) {
setFormData({
name: '',
email: '',
subject: '',
message: ''
})
setShowSuccess(true)
// Load new captcha
loadCaptcha()
} else {
console.error('Contact form error:', data.error)
setShowError(true)
// Reload captcha on error
loadCaptcha()
}
} catch (error) {
console.error('Contact form submission error:', error)
setShowError(true)
// Reload captcha on error
loadCaptcha()
} finally {
setIsSubmitting(false)
}
}
const contactInfo = [
{
icon: <Email sx={{ fontSize: 30, color: 'primary.main' }} />,
title: t('info.email.title'),
content: t('info.email.content'),
action: 'mailto:contact@biblical-guide.com'
}
]
return (
<Box sx={{ py: 4 }}>
{/* Hero Section */}
<Box
sx={{
background: 'linear-gradient(135deg, #009688 0%, #00796B 100%)',
color: 'white',
py: 8,
mb: 6,
}}
>
<Container maxWidth="lg">
<Box sx={{ textAlign: 'center' }}>
<ContactSupport sx={{ fontSize: 80, mb: 2, opacity: 0.9 }} />
<Typography variant="h2" component="h1" gutterBottom>
{t('hero.title')}
</Typography>
<Typography variant="h5" component="h2" sx={{ mb: 2, opacity: 0.9 }}>
{t('hero.subtitle')}
</Typography>
<Typography variant="body1" sx={{ opacity: 0.8, maxWidth: 600, mx: 'auto' }}>
{t('hero.description')}
</Typography>
</Box>
</Container>
</Box>
<Container maxWidth="lg">
<Box sx={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
{/* Contact Form */}
<Box sx={{ flex: { xs: '1 1 100%', md: '1 1 65%' } }}>
<Card sx={{ height: 'fit-content' }}>
<CardContent sx={{ p: 4 }}>
<Typography variant="h4" component="h2" gutterBottom>
{t('form.title')}
</Typography>
<Typography variant="body1" color="text.secondary" sx={{ mb: 4 }}>
{t('form.description')}
</Typography>
<Box component="form" onSubmit={handleSubmit} sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
<TextField
fullWidth
required
label={t('form.fields.name')}
value={formData.name}
onChange={handleInputChange('name')}
variant="outlined"
sx={{ flex: { xs: '1 1 100%', sm: '1 1 calc(50% - 8px)' } }}
/>
<TextField
fullWidth
required
type="email"
label={t('form.fields.email')}
value={formData.email}
onChange={handleInputChange('email')}
variant="outlined"
sx={{ flex: { xs: '1 1 100%', sm: '1 1 calc(50% - 8px)' } }}
/>
</Box>
<TextField
fullWidth
required
label={t('form.fields.subject')}
value={formData.subject}
onChange={handleInputChange('subject')}
variant="outlined"
/>
<TextField
fullWidth
required
multiline
rows={6}
label={t('form.fields.message')}
value={formData.message}
onChange={handleInputChange('message')}
variant="outlined"
/>
{/* Captcha */}
<Box sx={{
p: 3,
bgcolor: 'grey.50',
borderRadius: 2,
border: captchaError ? '2px solid' : '1px solid',
borderColor: captchaError ? 'error.main' : 'grey.300'
}}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
Security Check
</Typography>
<Button
size="small"
startIcon={<RefreshIcon />}
onClick={loadCaptcha}
disabled={isSubmitting}
sx={{ ml: 'auto' }}
>
New Question
</Button>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, flexWrap: 'wrap' }}>
<Typography variant="body1" sx={{ fontWeight: 500, fontSize: '1.1rem' }}>
What is {captcha.question}?
</Typography>
<TextField
required
type="number"
value={captcha.answer}
onChange={handleCaptchaChange}
error={captchaError}
helperText={captchaError ? 'Please answer the math question' : ''}
placeholder="Your answer"
sx={{
width: 120,
'& input': { textAlign: 'center', fontSize: '1.1rem' }
}}
inputProps={{
min: 0,
step: 1
}}
/>
</Box>
</Box>
<Box>
<Button
type="submit"
variant="contained"
size="large"
disabled={isSubmitting}
startIcon={<Send />}
sx={{ minWidth: 200 }}
>
{isSubmitting ? t('form.submitting') : t('form.submit')}
</Button>
</Box>
</Box>
</CardContent>
</Card>
</Box>
{/* Contact Information */}
<Box sx={{ flex: { xs: '1 1 100%', md: '1 1 35%' } }}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
<Typography variant="h4" component="h2">
{t('info.title')}
</Typography>
<Typography variant="body1" color="text.secondary" sx={{ mb: 2 }}>
{t('info.description')}
</Typography>
{contactInfo.map((info, index) => (
<Paper
key={index}
sx={{
p: 3,
display: 'flex',
alignItems: 'flex-start',
gap: 2,
cursor: info.action ? 'pointer' : 'default',
transition: 'transform 0.2s ease-in-out',
'&:hover': info.action ? {
transform: 'translateY(-2px)',
boxShadow: 2
} : {}
}}
onClick={() => info.action && window.open(info.action, '_self')}
>
<Box sx={{ flexShrink: 0 }}>
{info.icon}
</Box>
<Box>
<Typography variant="h6" sx={{ mb: 1 }}>
{info.title}
</Typography>
<Typography variant="body2" color="text.secondary">
{info.content}
</Typography>
</Box>
</Paper>
))}
{/* FAQ Quick Link */}
<Paper sx={{ p: 3, bgcolor: 'primary.light', color: 'white' }}>
<Typography variant="h6" sx={{ mb: 2 }}>
{t('faq.title')}
</Typography>
<Typography variant="body2" sx={{ mb: 3, opacity: 0.9 }}>
{t('faq.description')}
</Typography>
<Button
variant="outlined"
sx={{
color: 'white',
borderColor: 'white',
'&:hover': {
borderColor: 'white',
bgcolor: 'rgba(255,255,255,0.1)'
}
}}
onClick={() => router.push(`/${locale}#faq`)}
>
{t('faq.viewFaq')}
</Button>
</Paper>
</Box>
</Box>
</Box>
</Container>
{/* Success/Error Messages */}
<Snackbar
open={showSuccess}
autoHideDuration={6000}
onClose={() => setShowSuccess(false)}
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
>
<Alert onClose={() => setShowSuccess(false)} severity="success" sx={{ width: '100%' }}>
{t('form.success')}
</Alert>
</Snackbar>
<Snackbar
open={showError}
autoHideDuration={6000}
onClose={() => setShowError(false)}
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
>
<Alert onClose={() => setShowError(false)} severity="error" sx={{ width: '100%' }}>
{t('form.error')}
</Alert>
</Snackbar>
</Box>
)
}