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>
This commit is contained in:
@@ -19,7 +19,8 @@ import {
|
||||
} from '@mui/icons-material'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useTranslations, useLocale } from 'next-intl'
|
||||
import { useState } from 'react'
|
||||
import { useState, useEffect } from 'react'
|
||||
import RefreshIcon from '@mui/icons-material/Refresh'
|
||||
|
||||
export default function Contact() {
|
||||
const theme = useTheme()
|
||||
@@ -33,10 +34,37 @@ export default function Contact() {
|
||||
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,
|
||||
@@ -44,8 +72,23 @@ export default function Contact() {
|
||||
}))
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -54,7 +97,11 @@ export default function Contact() {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
body: JSON.stringify({
|
||||
...formData,
|
||||
captchaId: captcha.id,
|
||||
captchaAnswer: captcha.answer
|
||||
})
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
@@ -67,13 +114,19 @@ export default function Contact() {
|
||||
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)
|
||||
}
|
||||
@@ -171,6 +224,52 @@ export default function Contact() {
|
||||
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"
|
||||
|
||||
Reference in New Issue
Block a user