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>
383 lines
12 KiB
TypeScript
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>
|
|
)
|
|
} |