mirror of
https://github.com/JorySeverijnse/ui-fixer-supreme.git
synced 2026-01-29 17:58:38 +00:00
Improve human verification UX
Enhance HumanVerification to a Taobao-style slider puzzle and ensure the Sidebar's Access Granted folds in when space is limited without making the sidebar scrollable. Includes refinements to keep the UI responsive on mobile and clarify that the access text folds rather than scrolls. X-Lovable-Edit-ID: edt-560d57af-660c-4447-8352-e12d8094d2c8
This commit is contained in:
commit
6b586236bc
@ -9,23 +9,22 @@ interface HumanVerificationProps {
|
||||
onVerified: () => void;
|
||||
}
|
||||
|
||||
interface PuzzlePiece {
|
||||
targetX: number;
|
||||
}
|
||||
|
||||
const HumanVerification = ({ onVerified }: HumanVerificationProps) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const [puzzle, setPuzzle] = useState<PuzzlePiece | null>(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [sliderX, setSliderX] = useState(0);
|
||||
const [isVerified, setIsVerified] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
const [puzzleImage, setPuzzleImage] = useState<ImageData | null>(null);
|
||||
const [targetX, setTargetX] = useState(0);
|
||||
const trackRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const CANVAS_WIDTH = 320;
|
||||
const CANVAS_HEIGHT = 160;
|
||||
const PIECE_SIZE = 44;
|
||||
const TOLERANCE = 10;
|
||||
const CANVAS_WIDTH = 300;
|
||||
const CANVAS_HEIGHT = 150;
|
||||
const PIECE_WIDTH = 42;
|
||||
const PIECE_HEIGHT = 42;
|
||||
const NOTCH_SIZE = 10;
|
||||
const TOLERANCE = 6;
|
||||
|
||||
// Check for bypass in URL on mount
|
||||
useEffect(() => {
|
||||
@ -36,9 +35,156 @@ const HumanVerification = ({ onVerified }: HumanVerificationProps) => {
|
||||
}
|
||||
}, [onVerified]);
|
||||
|
||||
const drawPuzzlePiecePath = (ctx: CanvasRenderingContext2D, x: number, y: number) => {
|
||||
const w = PIECE_WIDTH;
|
||||
const h = PIECE_HEIGHT;
|
||||
const n = NOTCH_SIZE;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y);
|
||||
// Top edge
|
||||
ctx.lineTo(x + w, y);
|
||||
// Right edge with notch
|
||||
ctx.lineTo(x + w, y + h * 0.35);
|
||||
ctx.arc(x + w + n * 0.5, y + h * 0.5, n, -Math.PI * 0.5, Math.PI * 0.5, false);
|
||||
ctx.lineTo(x + w, y + h);
|
||||
// Bottom edge
|
||||
ctx.lineTo(x, y + h);
|
||||
// Left edge
|
||||
ctx.lineTo(x, y);
|
||||
ctx.closePath();
|
||||
};
|
||||
|
||||
const generatePuzzle = useCallback(() => {
|
||||
const targetX = Math.floor(Math.random() * (CANVAS_WIDTH - PIECE_SIZE - 100)) + 80;
|
||||
setPuzzle({ targetX });
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
// Get theme color
|
||||
const computedStyle = getComputedStyle(document.documentElement);
|
||||
const primaryHsl = computedStyle.getPropertyValue('--primary').trim();
|
||||
const hslParts = primaryHsl.split(' ');
|
||||
const h = parseFloat(hslParts[0]) || 120;
|
||||
const s = parseFloat(hslParts[1]) || 100;
|
||||
const l = parseFloat(hslParts[2]) || 50;
|
||||
|
||||
// Clear canvas
|
||||
ctx.fillStyle = '#0d0d0d';
|
||||
ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
|
||||
|
||||
// Create a visually interesting background pattern
|
||||
// Gradient base
|
||||
const gradient = ctx.createLinearGradient(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
|
||||
gradient.addColorStop(0, `hsla(${h}, ${s}%, ${l * 0.15}%, 1)`);
|
||||
gradient.addColorStop(0.5, `hsla(${h}, ${s}%, ${l * 0.08}%, 1)`);
|
||||
gradient.addColorStop(1, `hsla(${h}, ${s}%, ${l * 0.12}%, 1)`);
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
|
||||
|
||||
// Draw circuit-like patterns
|
||||
ctx.strokeStyle = `hsla(${h}, ${s}%, ${l}%, 0.15)`;
|
||||
ctx.lineWidth = 1;
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const startX = Math.random() * CANVAS_WIDTH;
|
||||
const startY = Math.random() * CANVAS_HEIGHT;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(startX, startY);
|
||||
// Draw horizontal then vertical line
|
||||
const midX = startX + (Math.random() - 0.5) * 80;
|
||||
ctx.lineTo(midX, startY);
|
||||
ctx.lineTo(midX, startY + (Math.random() - 0.5) * 60);
|
||||
ctx.stroke();
|
||||
// Draw node dots
|
||||
ctx.fillStyle = `hsla(${h}, ${s}%, ${l}%, 0.3)`;
|
||||
ctx.beginPath();
|
||||
ctx.arc(startX, startY, 2, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
// Matrix-style falling characters
|
||||
ctx.font = '11px monospace';
|
||||
ctx.fillStyle = `hsla(${h}, ${s}%, ${l}%, 0.2)`;
|
||||
const chars = '01アイウエオカキクケコ田由甲申電网';
|
||||
for (let i = 0; i < 40; i++) {
|
||||
const cx = Math.random() * CANVAS_WIDTH;
|
||||
const cy = Math.random() * CANVAS_HEIGHT;
|
||||
ctx.fillText(chars[Math.floor(Math.random() * chars.length)], cx, cy);
|
||||
}
|
||||
|
||||
// Generate target position (where the piece should go)
|
||||
const newTargetX = Math.floor(Math.random() * (CANVAS_WIDTH - PIECE_WIDTH - 100)) + 80;
|
||||
const targetY = (CANVAS_HEIGHT - PIECE_HEIGHT) / 2;
|
||||
setTargetX(newTargetX);
|
||||
|
||||
// Draw the target slot (dark cutout with glow)
|
||||
ctx.save();
|
||||
drawPuzzlePiecePath(ctx, newTargetX, targetY);
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = `hsla(${h}, ${s}%, ${l}%, 0.5)`;
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.stroke();
|
||||
|
||||
// Inner shadow effect
|
||||
ctx.shadowColor = `hsla(${h}, ${s}%, ${l}%, 0.3)`;
|
||||
ctx.shadowBlur = 4;
|
||||
ctx.shadowOffsetX = 0;
|
||||
ctx.shadowOffsetY = 0;
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
|
||||
// Extract the puzzle piece image from a slightly different position for variation
|
||||
const extractX = 10;
|
||||
const extractY = targetY;
|
||||
|
||||
// Create puzzle piece appearance
|
||||
const pieceCanvas = document.createElement('canvas');
|
||||
pieceCanvas.width = PIECE_WIDTH + NOTCH_SIZE + 4;
|
||||
pieceCanvas.height = PIECE_HEIGHT + 4;
|
||||
const pieceCtx = pieceCanvas.getContext('2d');
|
||||
if (pieceCtx) {
|
||||
// Draw piece background with pattern
|
||||
pieceCtx.fillStyle = `hsla(${h}, ${s}%, ${l}%, 0.9)`;
|
||||
drawPuzzlePiecePath(pieceCtx, 2, 2);
|
||||
pieceCtx.fill();
|
||||
|
||||
// Add texture
|
||||
pieceCtx.strokeStyle = `hsla(${h}, ${s}%, ${l * 1.3}%, 0.6)`;
|
||||
pieceCtx.lineWidth = 1.5;
|
||||
drawPuzzlePiecePath(pieceCtx, 2, 2);
|
||||
pieceCtx.stroke();
|
||||
|
||||
// Inner pattern
|
||||
pieceCtx.strokeStyle = 'rgba(0, 0, 0, 0.3)';
|
||||
pieceCtx.lineWidth = 1;
|
||||
pieceCtx.strokeRect(8, 8, PIECE_WIDTH - 12, PIECE_HEIGHT - 12);
|
||||
|
||||
// Arrow indicator
|
||||
pieceCtx.fillStyle = 'rgba(0, 0, 0, 0.5)';
|
||||
pieceCtx.beginPath();
|
||||
const arrowCx = PIECE_WIDTH / 2 + 2;
|
||||
const arrowCy = PIECE_HEIGHT / 2 + 2;
|
||||
pieceCtx.moveTo(arrowCx - 6, arrowCy - 5);
|
||||
pieceCtx.lineTo(arrowCx + 6, arrowCy);
|
||||
pieceCtx.lineTo(arrowCx - 6, arrowCy + 5);
|
||||
pieceCtx.closePath();
|
||||
pieceCtx.fill();
|
||||
|
||||
setPuzzleImage(pieceCtx.getImageData(0, 0, pieceCanvas.width, pieceCanvas.height));
|
||||
}
|
||||
|
||||
// Draw scanlines
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.06)';
|
||||
for (let y = 0; y < CANVAS_HEIGHT; y += 2) {
|
||||
ctx.fillRect(0, y, CANVAS_WIDTH, 1);
|
||||
}
|
||||
|
||||
// Border
|
||||
ctx.strokeStyle = `hsla(${h}, ${s}%, ${l}%, 0.4)`;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeRect(1, 1, CANVAS_WIDTH - 2, CANVAS_HEIGHT - 2);
|
||||
|
||||
setSliderX(0);
|
||||
setError(false);
|
||||
setIsVerified(false);
|
||||
@ -48,9 +194,20 @@ const HumanVerification = ({ onVerified }: HumanVerificationProps) => {
|
||||
generatePuzzle();
|
||||
}, [generatePuzzle]);
|
||||
|
||||
// Draw the puzzle piece at current position
|
||||
useEffect(() => {
|
||||
if (!puzzle || !canvasRef.current) return;
|
||||
if (!canvasRef.current || !puzzleImage) return;
|
||||
const canvas = canvasRef.current;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
// Regenerate base image
|
||||
generatePuzzle();
|
||||
}, []);
|
||||
|
||||
// Redraw canvas with piece at current slider position
|
||||
const redrawWithPiece = useCallback(() => {
|
||||
if (!canvasRef.current || !puzzleImage) return;
|
||||
const canvas = canvasRef.current;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
@ -62,121 +219,118 @@ const HumanVerification = ({ onVerified }: HumanVerificationProps) => {
|
||||
const h = parseFloat(hslParts[0]) || 120;
|
||||
const s = parseFloat(hslParts[1]) || 100;
|
||||
const l = parseFloat(hslParts[2]) || 50;
|
||||
const primaryColor = `hsl(${h}, ${s}%, ${l}%)`;
|
||||
const dimColor = `hsla(${h}, ${s}%, ${l}%, 0.3)`;
|
||||
const bgColor = `hsla(${h}, ${s}%, ${l}%, 0.05)`;
|
||||
|
||||
// Clear canvas with dark background
|
||||
ctx.fillStyle = '#0a0a0a';
|
||||
// Clear and redraw background
|
||||
ctx.fillStyle = '#0d0d0d';
|
||||
ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
|
||||
|
||||
// Draw matrix grid
|
||||
ctx.strokeStyle = dimColor;
|
||||
ctx.lineWidth = 0.5;
|
||||
for (let x = 0; x < CANVAS_WIDTH; x += 16) {
|
||||
const gradient = ctx.createLinearGradient(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
|
||||
gradient.addColorStop(0, `hsla(${h}, ${s}%, ${l * 0.15}%, 1)`);
|
||||
gradient.addColorStop(0.5, `hsla(${h}, ${s}%, ${l * 0.08}%, 1)`);
|
||||
gradient.addColorStop(1, `hsla(${h}, ${s}%, ${l * 0.12}%, 1)`);
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
|
||||
|
||||
// Circuit patterns
|
||||
ctx.strokeStyle = `hsla(${h}, ${s}%, ${l}%, 0.15)`;
|
||||
ctx.lineWidth = 1;
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const startX = (i * 37) % CANVAS_WIDTH;
|
||||
const startY = (i * 23) % CANVAS_HEIGHT;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, 0);
|
||||
ctx.lineTo(x, CANVAS_HEIGHT);
|
||||
ctx.stroke();
|
||||
}
|
||||
for (let y = 0; y < CANVAS_HEIGHT; y += 16) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, y);
|
||||
ctx.lineTo(CANVAS_WIDTH, y);
|
||||
ctx.moveTo(startX, startY);
|
||||
ctx.lineTo(startX + 40, startY);
|
||||
ctx.lineTo(startX + 40, startY + 30);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// Draw random matrix code in background
|
||||
ctx.font = '10px monospace';
|
||||
ctx.fillStyle = `hsla(${h}, ${s}%, ${l}%, 0.15)`;
|
||||
// Matrix chars
|
||||
ctx.font = '11px monospace';
|
||||
ctx.fillStyle = `hsla(${h}, ${s}%, ${l}%, 0.2)`;
|
||||
const chars = '01アイウエオ田由甲申';
|
||||
for (let i = 0; i < 30; i++) {
|
||||
const x = Math.random() * CANVAS_WIDTH;
|
||||
const y = Math.random() * CANVAS_HEIGHT;
|
||||
const chars = '01アイウエオカキクケコサシスセソ';
|
||||
ctx.fillText(chars[Math.floor(Math.random() * chars.length)], x, y);
|
||||
ctx.fillText(chars[i % chars.length], (i * 17) % CANVAS_WIDTH, (i * 13) % CANVAS_HEIGHT);
|
||||
}
|
||||
|
||||
// Target slot position
|
||||
const slotY = (CANVAS_HEIGHT - PIECE_SIZE) / 2;
|
||||
const pieceY = (CANVAS_HEIGHT - PIECE_HEIGHT) / 2;
|
||||
|
||||
// Draw target slot with glow
|
||||
ctx.shadowColor = primaryColor;
|
||||
ctx.shadowBlur = 8;
|
||||
ctx.fillStyle = bgColor;
|
||||
ctx.strokeStyle = dimColor;
|
||||
ctx.lineWidth = 2;
|
||||
|
||||
// Draw slot shape with puzzle notch
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(puzzle.targetX, slotY, PIECE_SIZE, PIECE_SIZE, 4);
|
||||
// Draw target slot
|
||||
ctx.save();
|
||||
drawPuzzlePiecePath(ctx, targetX, pieceY);
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = `hsla(${h}, ${s}%, ${l}%, 0.5)`;
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
|
||||
// Inner slot pattern
|
||||
ctx.shadowBlur = 0;
|
||||
ctx.strokeStyle = `hsla(${h}, ${s}%, ${l}%, 0.2)`;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(puzzle.targetX + 8, slotY + 8, PIECE_SIZE - 16, PIECE_SIZE - 16);
|
||||
ctx.strokeRect(puzzle.targetX + 14, slotY + 14, PIECE_SIZE - 28, PIECE_SIZE - 28);
|
||||
// Calculate piece position from slider
|
||||
const maxSlide = trackRef.current ? trackRef.current.clientWidth - 48 : 240;
|
||||
const pieceX = (sliderX / maxSlide) * (CANVAS_WIDTH - PIECE_WIDTH - NOTCH_SIZE - 10) + 5;
|
||||
|
||||
// Calculate piece position based on slider
|
||||
const maxSlide = trackRef.current ? trackRef.current.clientWidth - 48 : 280;
|
||||
const pieceX = (sliderX / maxSlide) * (CANVAS_WIDTH - PIECE_SIZE - 10) + 5;
|
||||
const pieceY = slotY;
|
||||
// Draw draggable piece with shadow/glow
|
||||
ctx.save();
|
||||
|
||||
let pieceColor = `hsla(${h}, ${s}%, ${l}%, 0.95)`;
|
||||
let borderColor = `hsla(${h}, ${s}%, ${l * 1.2}%, 1)`;
|
||||
let glowColor = `hsla(${h}, ${s}%, ${l}%, 0.6)`;
|
||||
|
||||
// Determine piece color
|
||||
let pieceColor = primaryColor;
|
||||
let pieceBorderColor = primaryColor;
|
||||
if (error) {
|
||||
pieceColor = 'hsl(0, 70%, 50%)';
|
||||
pieceBorderColor = 'hsl(0, 70%, 60%)';
|
||||
pieceColor = 'hsla(0, 70%, 50%, 0.95)';
|
||||
borderColor = 'hsl(0, 70%, 60%)';
|
||||
glowColor = 'hsla(0, 70%, 50%, 0.6)';
|
||||
} else if (isVerified) {
|
||||
pieceColor = 'hsl(142, 76%, 36%)';
|
||||
pieceBorderColor = 'hsl(142, 76%, 50%)';
|
||||
pieceColor = 'hsla(142, 76%, 40%, 0.95)';
|
||||
borderColor = 'hsl(142, 76%, 55%)';
|
||||
glowColor = 'hsla(142, 76%, 40%, 0.6)';
|
||||
}
|
||||
|
||||
// Draw puzzle piece with glow
|
||||
ctx.shadowColor = pieceBorderColor;
|
||||
// Glow effect
|
||||
ctx.shadowColor = glowColor;
|
||||
ctx.shadowBlur = 12;
|
||||
ctx.fillStyle = pieceColor;
|
||||
ctx.strokeStyle = pieceBorderColor;
|
||||
ctx.lineWidth = 2;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(pieceX, pieceY, PIECE_SIZE, PIECE_SIZE, 4);
|
||||
drawPuzzlePiecePath(ctx, pieceX, pieceY);
|
||||
ctx.fillStyle = pieceColor;
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = borderColor;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// Draw inner pattern on piece
|
||||
ctx.strokeStyle = 'rgba(0, 0, 0, 0.4)';
|
||||
// Inner pattern on piece
|
||||
ctx.strokeStyle = 'rgba(0, 0, 0, 0.3)';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(pieceX + 8, pieceY + 8, PIECE_SIZE - 16, PIECE_SIZE - 16);
|
||||
ctx.strokeRect(pieceX + 6, pieceY + 6, PIECE_WIDTH - 12, PIECE_HEIGHT - 12);
|
||||
|
||||
// Draw arrow on piece
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.6)';
|
||||
// Arrow
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
|
||||
ctx.beginPath();
|
||||
const arrowX = pieceX + PIECE_SIZE / 2;
|
||||
const arrowY = pieceY + PIECE_SIZE / 2;
|
||||
ctx.moveTo(arrowX - 6, arrowY - 5);
|
||||
ctx.lineTo(arrowX + 6, arrowY);
|
||||
ctx.lineTo(arrowX - 6, arrowY + 5);
|
||||
const arrowX = pieceX + PIECE_WIDTH / 2;
|
||||
const arrowY = pieceY + PIECE_HEIGHT / 2;
|
||||
ctx.moveTo(arrowX - 5, arrowY - 4);
|
||||
ctx.lineTo(arrowX + 5, arrowY);
|
||||
ctx.lineTo(arrowX - 5, arrowY + 4);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
|
||||
// Draw scanlines
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.08)';
|
||||
// Scanlines
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.05)';
|
||||
for (let y = 0; y < CANVAS_HEIGHT; y += 2) {
|
||||
ctx.fillRect(0, y, CANVAS_WIDTH, 1);
|
||||
}
|
||||
|
||||
// Draw border
|
||||
ctx.strokeStyle = dimColor;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(0.5, 0.5, CANVAS_WIDTH - 1, CANVAS_HEIGHT - 1);
|
||||
// Border
|
||||
ctx.strokeStyle = `hsla(${h}, ${s}%, ${l}%, 0.4)`;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeRect(1, 1, CANVAS_WIDTH - 2, CANVAS_HEIGHT - 2);
|
||||
|
||||
}, [puzzle, sliderX, error, isVerified]);
|
||||
}, [sliderX, error, isVerified, targetX, puzzleImage]);
|
||||
|
||||
useEffect(() => {
|
||||
redrawWithPiece();
|
||||
}, [sliderX, error, isVerified, redrawWithPiece]);
|
||||
|
||||
const handleMouseDown = () => {
|
||||
setIsDragging(true);
|
||||
@ -191,23 +345,23 @@ const HumanVerification = ({ onVerified }: HumanVerificationProps) => {
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
if (!isDragging || !puzzle || !trackRef.current) return;
|
||||
if (!isDragging || !trackRef.current) return;
|
||||
setIsDragging(false);
|
||||
|
||||
const maxSlide = trackRef.current.clientWidth - 48;
|
||||
const pieceX = (sliderX / maxSlide) * (CANVAS_WIDTH - PIECE_SIZE - 10) + 5;
|
||||
const diff = Math.abs(pieceX - puzzle.targetX);
|
||||
const pieceX = (sliderX / maxSlide) * (CANVAS_WIDTH - PIECE_WIDTH - NOTCH_SIZE - 10) + 5;
|
||||
const diff = Math.abs(pieceX - targetX);
|
||||
|
||||
if (diff <= TOLERANCE) {
|
||||
setIsVerified(true);
|
||||
localStorage.setItem(VERIFIED_KEY, 'true');
|
||||
setTimeout(() => onVerified(), 600);
|
||||
setTimeout(() => onVerified(), 500);
|
||||
} else {
|
||||
setError(true);
|
||||
setTimeout(() => {
|
||||
setSliderX(0);
|
||||
setError(false);
|
||||
}, 400);
|
||||
}, 350);
|
||||
}
|
||||
};
|
||||
|
||||
@ -229,40 +383,35 @@ const HumanVerification = ({ onVerified }: HumanVerificationProps) => {
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="w-full max-w-sm space-y-4"
|
||||
className="w-full max-w-[340px] space-y-3"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="text-center border border-primary/40 bg-primary/5 py-3 px-4">
|
||||
<div className="text-center border border-primary/40 bg-primary/5 py-2.5 px-4">
|
||||
<div className="text-primary font-mono text-sm tracking-wider font-bold">
|
||||
SECURITY VERIFICATION v3.0
|
||||
SECURITY VERIFICATION
|
||||
</div>
|
||||
<div className="text-primary/60 font-mono text-xs">
|
||||
ANTI-BOT PROTOCOL
|
||||
<div className="text-primary/50 font-mono text-[10px]">
|
||||
ANTI-BOT PROTOCOL v3.0
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Instructions */}
|
||||
<div className="text-primary font-mono text-sm">
|
||||
{'>'} Slide the piece to complete the puzzle
|
||||
</div>
|
||||
|
||||
{/* Canvas puzzle area */}
|
||||
<div className="border border-primary/40 bg-background overflow-hidden relative">
|
||||
<div className="border border-primary/30 overflow-hidden">
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
width={CANVAS_WIDTH}
|
||||
height={CANVAS_HEIGHT}
|
||||
className="w-full"
|
||||
className="w-full block"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Slider track */}
|
||||
<div
|
||||
ref={trackRef}
|
||||
className={`relative h-12 border-2 ${
|
||||
className={`relative h-11 border-2 ${
|
||||
error ? 'border-destructive bg-destructive/10' :
|
||||
isVerified ? 'border-green-500 bg-green-500/10' :
|
||||
'border-primary/40 bg-primary/5'
|
||||
'border-primary/30 bg-primary/5'
|
||||
} transition-colors duration-200 cursor-pointer select-none`}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseUp={handleMouseUp}
|
||||
@ -272,10 +421,10 @@ const HumanVerification = ({ onVerified }: HumanVerificationProps) => {
|
||||
>
|
||||
{/* Track label */}
|
||||
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
||||
<span className={`font-mono text-xs ${
|
||||
isVerified ? 'text-green-500' : 'text-primary/40'
|
||||
<span className={`font-mono text-xs tracking-wide ${
|
||||
isVerified ? 'text-green-500' : 'text-primary/30'
|
||||
}`}>
|
||||
{isVerified ? '[ ACCESS GRANTED ]' : '>>> SLIDE TO VERIFY >>>'}
|
||||
{isVerified ? '[ VERIFIED ]' : 'DRAG TO COMPLETE PUZZLE'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -290,28 +439,22 @@ const HumanVerification = ({ onVerified }: HumanVerificationProps) => {
|
||||
onMouseDown={handleMouseDown}
|
||||
onTouchStart={handleTouchStart}
|
||||
animate={error ? { x: [0, -4, 4, -4, 4, 0] } : {}}
|
||||
transition={{ duration: 0.25 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<ChevronRight className="w-5 h-5 text-background" />
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Refresh */}
|
||||
<div className="flex justify-between items-center text-xs font-mono text-muted-foreground">
|
||||
<span>Protocol: SLIDER-VERIFY-3.0</span>
|
||||
{/* Controls */}
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={generatePuzzle}
|
||||
className="flex items-center gap-1 text-primary hover:text-primary/80 transition-colors"
|
||||
className="flex items-center gap-1.5 text-xs font-mono text-primary/60 hover:text-primary transition-colors"
|
||||
>
|
||||
<RefreshCw className="w-3 h-3" />
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="text-center text-muted-foreground/50 font-mono text-xs">
|
||||
// This verification helps protect against automated access
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -77,7 +77,7 @@ const Sidebar = () => {
|
||||
</button>
|
||||
|
||||
{/* Desktop Navigation - Always visible */}
|
||||
<nav ref={navRef} className="hidden md:block flex-grow overflow-y-auto p-4 md:p-5">
|
||||
<nav ref={navRef} className="hidden md:flex flex-col flex-grow p-4 md:p-5">
|
||||
{navItems.map((item, index) => {
|
||||
const isActive = location.pathname === item.path;
|
||||
|
||||
@ -108,9 +108,9 @@ const Sidebar = () => {
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Inline footer when sidebar is too short */}
|
||||
{/* Inline footer when sidebar is too short - folded into nav */}
|
||||
{!showFooter && (
|
||||
<div className="mt-4 pt-4 border-t border-primary/30">
|
||||
<div className="mt-auto pt-4 border-t border-primary/30">
|
||||
<p className="font-pixel text-sm text-primary text-glow text-center">
|
||||
Access Granted
|
||||
</p>
|
||||
|
||||
@ -1,318 +0,0 @@
|
||||
import { useState, useRef, useEffect, useCallback } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { RefreshCw, ChevronRight } from 'lucide-react';
|
||||
|
||||
interface SliderVerificationProps {
|
||||
onVerified: () => void;
|
||||
}
|
||||
|
||||
interface PuzzlePiece {
|
||||
targetX: number;
|
||||
currentX: number;
|
||||
}
|
||||
|
||||
export const SliderVerification = ({ onVerified }: SliderVerificationProps) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const [puzzle, setPuzzle] = useState<PuzzlePiece | null>(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [sliderX, setSliderX] = useState(0);
|
||||
const [isVerified, setIsVerified] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
const sliderRef = useRef<HTMLDivElement>(null);
|
||||
const trackRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const CANVAS_WIDTH = 320;
|
||||
const CANVAS_HEIGHT = 180;
|
||||
const PIECE_SIZE = 50;
|
||||
const TOLERANCE = 8;
|
||||
|
||||
const generatePuzzle = useCallback(() => {
|
||||
const targetX = Math.floor(Math.random() * (CANVAS_WIDTH - PIECE_SIZE - 80)) + 60;
|
||||
setPuzzle({ targetX, currentX: 0 });
|
||||
setSliderX(0);
|
||||
setError(false);
|
||||
setIsVerified(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
generatePuzzle();
|
||||
}, [generatePuzzle]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!puzzle || !canvasRef.current) return;
|
||||
|
||||
const canvas = canvasRef.current;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
// Clear canvas
|
||||
ctx.fillStyle = 'hsl(var(--background))';
|
||||
ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
|
||||
|
||||
// Draw matrix-style grid pattern
|
||||
ctx.strokeStyle = 'hsl(var(--primary) / 0.1)';
|
||||
ctx.lineWidth = 1;
|
||||
for (let x = 0; x < CANVAS_WIDTH; x += 20) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, 0);
|
||||
ctx.lineTo(x, CANVAS_HEIGHT);
|
||||
ctx.stroke();
|
||||
}
|
||||
for (let y = 0; y < CANVAS_HEIGHT; y += 20) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, y);
|
||||
ctx.lineTo(CANVAS_WIDTH, y);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// Draw random "code" elements
|
||||
ctx.fillStyle = 'hsl(var(--primary) / 0.15)';
|
||||
ctx.font = '10px monospace';
|
||||
for (let i = 0; i < 15; i++) {
|
||||
const x = Math.random() * CANVAS_WIDTH;
|
||||
const y = Math.random() * CANVAS_HEIGHT;
|
||||
const chars = ['0', '1', 'A', 'F', 'X', '#', '@'];
|
||||
ctx.fillText(chars[Math.floor(Math.random() * chars.length)], x, y);
|
||||
}
|
||||
|
||||
// Draw target slot (where puzzle piece should go)
|
||||
const slotY = (CANVAS_HEIGHT - PIECE_SIZE) / 2;
|
||||
ctx.fillStyle = 'hsl(var(--primary) / 0.2)';
|
||||
ctx.strokeStyle = 'hsl(var(--primary) / 0.5)';
|
||||
ctx.lineWidth = 2;
|
||||
|
||||
// Draw puzzle slot with notch
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(puzzle.targetX, slotY);
|
||||
ctx.lineTo(puzzle.targetX + PIECE_SIZE, slotY);
|
||||
ctx.lineTo(puzzle.targetX + PIECE_SIZE, slotY + PIECE_SIZE);
|
||||
ctx.lineTo(puzzle.targetX, slotY + PIECE_SIZE);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
|
||||
// Draw inner pattern for slot
|
||||
ctx.strokeStyle = 'hsl(var(--primary) / 0.3)';
|
||||
ctx.lineWidth = 1;
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const offset = i * 12 + 8;
|
||||
ctx.strokeRect(
|
||||
puzzle.targetX + offset / 2,
|
||||
slotY + offset / 2,
|
||||
PIECE_SIZE - offset,
|
||||
PIECE_SIZE - offset
|
||||
);
|
||||
}
|
||||
|
||||
// Draw the draggable puzzle piece at current position
|
||||
const pieceX = sliderX * (CANVAS_WIDTH - PIECE_SIZE) / (trackRef.current?.clientWidth || 280);
|
||||
const pieceY = slotY;
|
||||
|
||||
// Piece shadow/glow
|
||||
ctx.shadowColor = 'hsl(var(--primary))';
|
||||
ctx.shadowBlur = 10;
|
||||
|
||||
ctx.fillStyle = error
|
||||
? 'hsl(var(--destructive) / 0.8)'
|
||||
: isVerified
|
||||
? 'hsl(142 76% 36% / 0.8)'
|
||||
: 'hsl(var(--primary) / 0.8)';
|
||||
ctx.strokeStyle = error
|
||||
? 'hsl(var(--destructive))'
|
||||
: isVerified
|
||||
? 'hsl(142 76% 36%)'
|
||||
: 'hsl(var(--primary))';
|
||||
ctx.lineWidth = 2;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(pieceX, pieceY);
|
||||
ctx.lineTo(pieceX + PIECE_SIZE, pieceY);
|
||||
ctx.lineTo(pieceX + PIECE_SIZE, pieceY + PIECE_SIZE);
|
||||
ctx.lineTo(pieceX, pieceY + PIECE_SIZE);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// Draw inner pattern for piece
|
||||
ctx.strokeStyle = 'hsl(var(--background) / 0.5)';
|
||||
ctx.lineWidth = 1;
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const offset = i * 12 + 8;
|
||||
ctx.strokeRect(
|
||||
pieceX + offset / 2,
|
||||
pieceY + offset / 2,
|
||||
PIECE_SIZE - offset,
|
||||
PIECE_SIZE - offset
|
||||
);
|
||||
}
|
||||
|
||||
// Draw arrow indicator on piece
|
||||
ctx.fillStyle = 'hsl(var(--background))';
|
||||
ctx.beginPath();
|
||||
const arrowX = pieceX + PIECE_SIZE / 2;
|
||||
const arrowY = pieceY + PIECE_SIZE / 2;
|
||||
ctx.moveTo(arrowX - 8, arrowY - 5);
|
||||
ctx.lineTo(arrowX + 5, arrowY);
|
||||
ctx.lineTo(arrowX - 8, arrowY + 5);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
}, [puzzle, sliderX, error, isVerified]);
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
setIsDragging(true);
|
||||
setError(false);
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: React.MouseEvent) => {
|
||||
if (!isDragging || !trackRef.current) return;
|
||||
|
||||
const rect = trackRef.current.getBoundingClientRect();
|
||||
const x = Math.max(0, Math.min(e.clientX - rect.left - 20, rect.width - 40));
|
||||
setSliderX(x);
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
if (!isDragging || !puzzle || !trackRef.current) return;
|
||||
setIsDragging(false);
|
||||
|
||||
// Calculate if piece is in correct position
|
||||
const pieceX = sliderX * (CANVAS_WIDTH - PIECE_SIZE) / (trackRef.current.clientWidth - 40);
|
||||
const diff = Math.abs(pieceX - puzzle.targetX);
|
||||
|
||||
if (diff <= TOLERANCE) {
|
||||
setIsVerified(true);
|
||||
setTimeout(() => {
|
||||
onVerified();
|
||||
}, 800);
|
||||
} else {
|
||||
setError(true);
|
||||
setTimeout(() => {
|
||||
setSliderX(0);
|
||||
setError(false);
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTouchStart = (e: React.TouchEvent) => {
|
||||
setIsDragging(true);
|
||||
setError(false);
|
||||
};
|
||||
|
||||
const handleTouchMove = (e: React.TouchEvent) => {
|
||||
if (!isDragging || !trackRef.current) return;
|
||||
|
||||
const touch = e.touches[0];
|
||||
const rect = trackRef.current.getBoundingClientRect();
|
||||
const x = Math.max(0, Math.min(touch.clientX - rect.left - 20, rect.width - 40));
|
||||
setSliderX(x);
|
||||
};
|
||||
|
||||
const handleTouchEnd = () => {
|
||||
handleMouseUp();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 bg-background flex items-center justify-center p-4">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="w-full max-w-md space-y-4"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="text-center border border-primary/30 bg-primary/5 py-3 px-4">
|
||||
<div className="text-primary font-mono text-sm tracking-wider">
|
||||
SECURITY VERIFICATION v3.0
|
||||
</div>
|
||||
<div className="text-primary/60 font-mono text-xs">
|
||||
ANTI-BOT PROTOCOL
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Instructions */}
|
||||
<div className="text-primary font-mono text-sm">
|
||||
{'>'} Slide the puzzle piece to complete verification
|
||||
</div>
|
||||
|
||||
{/* Canvas puzzle area */}
|
||||
<div className="border border-primary/40 bg-background p-1 relative overflow-hidden">
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
width={CANVAS_WIDTH}
|
||||
height={CANVAS_HEIGHT}
|
||||
className="w-full"
|
||||
style={{ imageRendering: 'pixelated' }}
|
||||
/>
|
||||
|
||||
{/* Scanline effect */}
|
||||
<div
|
||||
className="absolute inset-0 pointer-events-none opacity-20"
|
||||
style={{
|
||||
background: 'repeating-linear-gradient(0deg, transparent, transparent 2px, hsl(var(--primary) / 0.03) 2px, hsl(var(--primary) / 0.03) 4px)'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Slider track */}
|
||||
<div
|
||||
ref={trackRef}
|
||||
className={`relative h-12 border-2 ${
|
||||
error ? 'border-destructive bg-destructive/10' :
|
||||
isVerified ? 'border-green-500 bg-green-500/10' :
|
||||
'border-primary/40 bg-primary/5'
|
||||
} transition-colors duration-200 cursor-pointer select-none`}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseUp={handleMouseUp}
|
||||
onMouseLeave={handleMouseUp}
|
||||
onTouchMove={handleTouchMove}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
>
|
||||
{/* Track label */}
|
||||
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
||||
<span className={`font-mono text-sm ${
|
||||
isVerified ? 'text-green-500' : 'text-primary/40'
|
||||
}`}>
|
||||
{isVerified ? '[ VERIFIED ]' : '>>> SLIDE TO VERIFY >>>'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Slider handle */}
|
||||
<motion.div
|
||||
ref={sliderRef}
|
||||
className={`absolute top-0 h-full w-12 flex items-center justify-center cursor-grab active:cursor-grabbing ${
|
||||
error ? 'bg-destructive' :
|
||||
isVerified ? 'bg-green-500' :
|
||||
'bg-primary'
|
||||
} transition-colors duration-200`}
|
||||
style={{ left: sliderX }}
|
||||
onMouseDown={handleMouseDown}
|
||||
onTouchStart={handleTouchStart}
|
||||
animate={error ? { x: [0, -5, 5, -5, 5, 0] } : {}}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<ChevronRight className="w-6 h-6 text-background" />
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Refresh button */}
|
||||
<div className="flex justify-between items-center text-xs font-mono text-muted-foreground">
|
||||
<span>Protocol: SLIDER-VERIFY-3.0</span>
|
||||
<button
|
||||
onClick={generatePuzzle}
|
||||
className="flex items-center gap-1 text-primary hover:text-primary/80 transition-colors"
|
||||
>
|
||||
<RefreshCw className="w-3 h-3" />
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="text-center text-muted-foreground/50 font-mono text-xs">
|
||||
// This verification helps protect against automated access
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user