Files
thrilltrack-explorer/src-old/components/auth/TurnstileCaptcha.tsx

150 lines
4.2 KiB
TypeScript

import { useEffect, useRef, useState } from 'react';
import { Turnstile } from '@marsidev/react-turnstile';
import { Callout, CalloutDescription } from '@/components/ui/callout';
import { AlertCircle, RefreshCw } from 'lucide-react';
import { Button } from '@/components/ui/button';
interface TurnstileCaptchaProps {
onSuccess: (token: string) => void;
onError?: (error: string) => void;
onExpire?: () => void;
siteKey?: string;
theme?: 'light' | 'dark' | 'auto';
size?: 'normal' | 'compact';
className?: string;
}
export function TurnstileCaptcha({
onSuccess,
onError,
onExpire,
siteKey = import.meta.env.VITE_TURNSTILE_SITE_KEY,
theme = 'auto',
size = 'normal',
className = ''
}: TurnstileCaptchaProps) {
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [key, setKey] = useState(0);
const turnstileRef = useRef(null);
const handleSuccess = (token: string) => {
setError(null);
setLoading(false);
onSuccess(token);
};
const handleError = (errorCode: string) => {
setLoading(false);
const errorMessage = getErrorMessage(errorCode);
setError(errorMessage);
onError?.(errorMessage);
};
const handleExpire = () => {
setError('CAPTCHA expired. Please try again.');
onExpire?.();
};
const handleLoad = () => {
setLoading(false);
setError(null);
};
const resetCaptcha = () => {
setKey(prev => prev + 1);
setError(null);
setLoading(true);
};
const getErrorMessage = (errorCode: string): string => {
switch (errorCode) {
case 'network-error':
return 'Network error. Please check your connection and try again.';
case 'timeout':
return 'CAPTCHA timed out. Please try again.';
case 'invalid-sitekey':
return 'Invalid site configuration. Please contact support.';
case 'token-already-spent':
return 'CAPTCHA token already used. Please refresh and try again.';
default:
return 'CAPTCHA verification failed. Please try again.';
}
};
// Monitor for initialization failures
useEffect(() => {
if (loading) {
const timeout = setTimeout(() => {
setLoading(false);
}, 5000); // 5 second timeout
return () => clearTimeout(timeout);
}
}, [loading]);
if (!siteKey) {
return (
<Callout variant="warning">
<AlertCircle className="h-4 w-4" />
<CalloutDescription>
CAPTCHA is not configured. Please set VITE_TURNSTILE_SITE_KEY environment variable.
</CalloutDescription>
</Callout>
);
}
return (
<div className={`space-y-3 ${className}`}>
<div className="flex flex-col items-center">
{loading && (
<div className="w-[300px] h-[65px] flex items-center justify-center border border-dashed border-muted-foreground/30 rounded-lg bg-muted/10 animate-pulse">
<span className="text-xs text-muted-foreground">Loading CAPTCHA...</span>
</div>
)}
<div
className="transition-opacity duration-100"
style={{
display: loading ? 'none' : 'block',
opacity: loading ? 0 : 1
}}
>
<Turnstile
key={key}
ref={turnstileRef}
siteKey={siteKey}
onSuccess={handleSuccess}
onError={handleError}
onExpire={handleExpire}
onLoad={handleLoad}
options={{
theme,
size,
execution: 'render',
appearance: 'always',
retry: 'auto'
}}
/>
</div>
</div>
{error && (
<Callout variant="destructive">
<AlertCircle className="h-4 w-4" />
<CalloutDescription className="flex items-center justify-between">
<span>{error}</span>
<Button
variant="outline"
size="sm"
onClick={resetCaptcha}
className="ml-2 h-6 px-2 text-xs"
>
<RefreshCw className="w-3 h-3 mr-1" />
Retry
</Button>
</CalloutDescription>
</Callout>
)}
</div>
);
}