Add TurnstileCaptcha component and update send API handler
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { assetUrl } from '../lib/assetUrl';
|
||||
import { Mail, Phone, Clock, ArrowUpRight, Check } from 'lucide-react';
|
||||
import { Mail, Phone, Clock, ArrowUpRight, Check, ShieldCheck } from 'lucide-react';
|
||||
import TurnstileCaptcha from './TurnstileCaptcha';
|
||||
|
||||
export default function ContactSection({ isMobile, onOpenStrategy }) {
|
||||
const [formData, setFormData] = useState({
|
||||
@@ -11,10 +12,21 @@ export default function ContactSection({ isMobile, onOpenStrategy }) {
|
||||
subject: '',
|
||||
message: ''
|
||||
});
|
||||
|
||||
const [turnstileToken, setTurnstileToken] = useState('');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
const [errorMsg, setErrorMsg] = useState('');
|
||||
|
||||
const handleTurnstileVerify = useCallback((token) => {
|
||||
setTurnstileToken(token);
|
||||
if (errorMsg) setErrorMsg('');
|
||||
}, [errorMsg]);
|
||||
|
||||
const handleTurnstileExpire = useCallback(() => {
|
||||
setTurnstileToken('');
|
||||
}, []);
|
||||
|
||||
const handleChange = (e) => {
|
||||
setFormData({ ...formData, [e.target.name]: e.target.value });
|
||||
if (errorMsg) setErrorMsg('');
|
||||
@@ -27,6 +39,11 @@ export default function ContactSection({ isMobile, onOpenStrategy }) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!turnstileToken) {
|
||||
setErrorMsg('Please complete the Cloudflare Turnstile security verification.');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
setErrorMsg('');
|
||||
|
||||
@@ -40,7 +57,8 @@ export default function ContactSection({ isMobile, onOpenStrategy }) {
|
||||
email: formData.email,
|
||||
phone: formData.phone,
|
||||
subject: formData.subject,
|
||||
message: formData.message
|
||||
message: formData.message,
|
||||
turnstileToken
|
||||
})
|
||||
});
|
||||
|
||||
@@ -54,7 +72,8 @@ export default function ContactSection({ isMobile, onOpenStrategy }) {
|
||||
email: formData.email,
|
||||
phone: formData.phone,
|
||||
subject: formData.subject,
|
||||
message: formData.message
|
||||
message: formData.message,
|
||||
turnstileToken
|
||||
})
|
||||
});
|
||||
}
|
||||
@@ -64,13 +83,15 @@ export default function ContactSection({ isMobile, onOpenStrategy }) {
|
||||
if (response.ok && resData.success) {
|
||||
setSubmitted(true);
|
||||
setFormData({ name: '', email: '', phone: '', subject: '', message: '' });
|
||||
setTurnstileToken('');
|
||||
} else {
|
||||
setErrorMsg(resData.error || resData.smtpError || 'Failed to send message. Please check SMTP configuration.');
|
||||
setErrorMsg(resData.error || resData.smtpError || 'Cloudflare Turnstile verification failed. Please try again.');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Contact submit error:', err);
|
||||
setSubmitted(true);
|
||||
setFormData({ name: '', email: '', phone: '', subject: '', message: '' });
|
||||
setTurnstileToken('');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
@@ -543,6 +564,12 @@ export default function ContactSection({ isMobile, onOpenStrategy }) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Cloudflare Turnstile Security Verification */}
|
||||
<TurnstileCaptcha
|
||||
onVerify={handleTurnstileVerify}
|
||||
onExpire={handleTurnstileExpire}
|
||||
/>
|
||||
|
||||
{/* Submit Button */}
|
||||
<button
|
||||
type="submit"
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
// Cloudflare Turnstile Test Sitekeys:
|
||||
// '1x00000000000000000000AA' -> Always passes (Visible Widget)
|
||||
const DEFAULT_SITE_KEY = '1x00000000000000000000AA';
|
||||
|
||||
export default function TurnstileCaptcha({ onVerify, onError, onExpire }) {
|
||||
const containerRef = useRef(null);
|
||||
const widgetIdRef = useRef(null);
|
||||
const [scriptLoaded, setScriptLoaded] = useState(false);
|
||||
|
||||
const siteKey = import.meta.env.VITE_TURNSTILE_SITE_KEY || process.env.VITE_TURNSTILE_SITE_KEY || DEFAULT_SITE_KEY;
|
||||
|
||||
useEffect(() => {
|
||||
if (window.turnstile) {
|
||||
setScriptLoaded(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const scriptId = 'cf-turnstile-script';
|
||||
let existingScript = document.getElementById(scriptId);
|
||||
|
||||
if (!existingScript) {
|
||||
const script = document.createElement('script');
|
||||
script.id = scriptId;
|
||||
script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit';
|
||||
script.async = true;
|
||||
script.defer = true;
|
||||
script.onload = () => setScriptLoaded(true);
|
||||
document.head.appendChild(script);
|
||||
} else {
|
||||
existingScript.addEventListener('load', () => setScriptLoaded(true));
|
||||
if (window.turnstile) setScriptLoaded(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!scriptLoaded || !containerRef.current || !window.turnstile) return;
|
||||
|
||||
if (widgetIdRef.current !== null) {
|
||||
try {
|
||||
window.turnstile.remove(widgetIdRef.current);
|
||||
} catch {
|
||||
// Ignore cleanup error if widget was already removed
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
widgetIdRef.current = window.turnstile.render(containerRef.current, {
|
||||
sitekey: siteKey,
|
||||
theme: 'dark',
|
||||
callback: (token) => {
|
||||
if (onVerify) onVerify(token);
|
||||
},
|
||||
'error-callback': (err) => {
|
||||
if (onError) onError(err);
|
||||
},
|
||||
'expired-callback': () => {
|
||||
if (onExpire) onExpire();
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('[Turnstile] Render error:', e);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (widgetIdRef.current !== null && window.turnstile) {
|
||||
try {
|
||||
window.turnstile.remove(widgetIdRef.current);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
widgetIdRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [scriptLoaded, siteKey]);
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
margin: '0.75rem 0',
|
||||
minHeight: '65px',
|
||||
width: '100%'
|
||||
}}>
|
||||
<div ref={containerRef} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user