mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-21 20:51:17 -05:00
150 lines
4.2 KiB
TypeScript
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>
|
|
);
|
|
} |