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:
gpt-engineer-app[bot] 2025-12-09 16:42:56 +00:00
commit 6b586236bc
3 changed files with 273 additions and 448 deletions

View File

@ -9,23 +9,22 @@ interface HumanVerificationProps {
onVerified: () => void; onVerified: () => void;
} }
interface PuzzlePiece {
targetX: number;
}
const HumanVerification = ({ onVerified }: HumanVerificationProps) => { const HumanVerification = ({ onVerified }: HumanVerificationProps) => {
const canvasRef = useRef<HTMLCanvasElement>(null); const canvasRef = useRef<HTMLCanvasElement>(null);
const [puzzle, setPuzzle] = useState<PuzzlePiece | null>(null);
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
const [sliderX, setSliderX] = useState(0); const [sliderX, setSliderX] = useState(0);
const [isVerified, setIsVerified] = useState(false); const [isVerified, setIsVerified] = useState(false);
const [error, setError] = 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 trackRef = useRef<HTMLDivElement>(null);
const CANVAS_WIDTH = 320; const CANVAS_WIDTH = 300;
const CANVAS_HEIGHT = 160; const CANVAS_HEIGHT = 150;
const PIECE_SIZE = 44; const PIECE_WIDTH = 42;
const TOLERANCE = 10; const PIECE_HEIGHT = 42;
const NOTCH_SIZE = 10;
const TOLERANCE = 6;
// Check for bypass in URL on mount // Check for bypass in URL on mount
useEffect(() => { useEffect(() => {
@ -36,9 +35,156 @@ const HumanVerification = ({ onVerified }: HumanVerificationProps) => {
} }
}, [onVerified]); }, [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 generatePuzzle = useCallback(() => {
const targetX = Math.floor(Math.random() * (CANVAS_WIDTH - PIECE_SIZE - 100)) + 80; const canvas = canvasRef.current;
setPuzzle({ targetX }); 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); setSliderX(0);
setError(false); setError(false);
setIsVerified(false); setIsVerified(false);
@ -48,9 +194,20 @@ const HumanVerification = ({ onVerified }: HumanVerificationProps) => {
generatePuzzle(); generatePuzzle();
}, [generatePuzzle]); }, [generatePuzzle]);
// Draw the puzzle piece at current position
useEffect(() => { 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 canvas = canvasRef.current;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
if (!ctx) return; if (!ctx) return;
@ -62,121 +219,118 @@ const HumanVerification = ({ onVerified }: HumanVerificationProps) => {
const h = parseFloat(hslParts[0]) || 120; const h = parseFloat(hslParts[0]) || 120;
const s = parseFloat(hslParts[1]) || 100; const s = parseFloat(hslParts[1]) || 100;
const l = parseFloat(hslParts[2]) || 50; 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 // Clear and redraw background
ctx.fillStyle = '#0a0a0a'; ctx.fillStyle = '#0d0d0d';
ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
// Draw matrix grid const gradient = ctx.createLinearGradient(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
ctx.strokeStyle = dimColor; gradient.addColorStop(0, `hsla(${h}, ${s}%, ${l * 0.15}%, 1)`);
ctx.lineWidth = 0.5; gradient.addColorStop(0.5, `hsla(${h}, ${s}%, ${l * 0.08}%, 1)`);
for (let x = 0; x < CANVAS_WIDTH; x += 16) { 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.beginPath();
ctx.moveTo(x, 0); ctx.moveTo(startX, startY);
ctx.lineTo(x, CANVAS_HEIGHT); ctx.lineTo(startX + 40, startY);
ctx.stroke(); ctx.lineTo(startX + 40, startY + 30);
}
for (let y = 0; y < CANVAS_HEIGHT; y += 16) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(CANVAS_WIDTH, y);
ctx.stroke(); ctx.stroke();
} }
// Draw random matrix code in background // Matrix chars
ctx.font = '10px monospace'; ctx.font = '11px monospace';
ctx.fillStyle = `hsla(${h}, ${s}%, ${l}%, 0.15)`; ctx.fillStyle = `hsla(${h}, ${s}%, ${l}%, 0.2)`;
const chars = '01アイウエオ田由甲申';
for (let i = 0; i < 30; i++) { for (let i = 0; i < 30; i++) {
const x = Math.random() * CANVAS_WIDTH; ctx.fillText(chars[i % chars.length], (i * 17) % CANVAS_WIDTH, (i * 13) % CANVAS_HEIGHT);
const y = Math.random() * CANVAS_HEIGHT;
const chars = '01アイウエオカキクケコサシスセソ';
ctx.fillText(chars[Math.floor(Math.random() * chars.length)], x, y);
} }
// Target slot position const pieceY = (CANVAS_HEIGHT - PIECE_HEIGHT) / 2;
const slotY = (CANVAS_HEIGHT - PIECE_SIZE) / 2;
// Draw target slot
// Draw target slot with glow ctx.save();
ctx.shadowColor = primaryColor; drawPuzzlePiecePath(ctx, targetX, pieceY);
ctx.shadowBlur = 8; ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
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);
ctx.fill(); ctx.fill();
ctx.strokeStyle = `hsla(${h}, ${s}%, ${l}%, 0.5)`;
ctx.lineWidth = 1.5;
ctx.stroke(); ctx.stroke();
ctx.restore();
// 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;
// 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)`;
// 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 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;
// Determine piece color
let pieceColor = primaryColor;
let pieceBorderColor = primaryColor;
if (error) { if (error) {
pieceColor = 'hsl(0, 70%, 50%)'; pieceColor = 'hsla(0, 70%, 50%, 0.95)';
pieceBorderColor = 'hsl(0, 70%, 60%)'; borderColor = 'hsl(0, 70%, 60%)';
glowColor = 'hsla(0, 70%, 50%, 0.6)';
} else if (isVerified) { } else if (isVerified) {
pieceColor = 'hsl(142, 76%, 36%)'; pieceColor = 'hsla(142, 76%, 40%, 0.95)';
pieceBorderColor = 'hsl(142, 76%, 50%)'; borderColor = 'hsl(142, 76%, 55%)';
glowColor = 'hsla(142, 76%, 40%, 0.6)';
} }
// Draw puzzle piece with glow // Glow effect
ctx.shadowColor = pieceBorderColor; ctx.shadowColor = glowColor;
ctx.shadowBlur = 12; ctx.shadowBlur = 12;
drawPuzzlePiecePath(ctx, pieceX, pieceY);
ctx.fillStyle = pieceColor; ctx.fillStyle = pieceColor;
ctx.strokeStyle = pieceBorderColor;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.roundRect(pieceX, pieceY, PIECE_SIZE, PIECE_SIZE, 4);
ctx.fill(); ctx.fill();
ctx.strokeStyle = borderColor;
ctx.lineWidth = 2;
ctx.stroke(); ctx.stroke();
ctx.shadowBlur = 0; ctx.shadowBlur = 0;
// Draw inner pattern on piece // Inner pattern on piece
ctx.strokeStyle = 'rgba(0, 0, 0, 0.4)'; ctx.strokeStyle = 'rgba(0, 0, 0, 0.3)';
ctx.lineWidth = 1; 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 // Arrow
ctx.fillStyle = 'rgba(0, 0, 0, 0.6)'; ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
ctx.beginPath(); ctx.beginPath();
const arrowX = pieceX + PIECE_SIZE / 2; const arrowX = pieceX + PIECE_WIDTH / 2;
const arrowY = pieceY + PIECE_SIZE / 2; const arrowY = pieceY + PIECE_HEIGHT / 2;
ctx.moveTo(arrowX - 6, arrowY - 5); ctx.moveTo(arrowX - 5, arrowY - 4);
ctx.lineTo(arrowX + 6, arrowY); ctx.lineTo(arrowX + 5, arrowY);
ctx.lineTo(arrowX - 6, arrowY + 5); ctx.lineTo(arrowX - 5, arrowY + 4);
ctx.closePath(); ctx.closePath();
ctx.fill(); ctx.fill();
ctx.restore();
// Draw scanlines // Scanlines
ctx.fillStyle = 'rgba(0, 0, 0, 0.08)'; ctx.fillStyle = 'rgba(0, 0, 0, 0.05)';
for (let y = 0; y < CANVAS_HEIGHT; y += 2) { for (let y = 0; y < CANVAS_HEIGHT; y += 2) {
ctx.fillRect(0, y, CANVAS_WIDTH, 1); ctx.fillRect(0, y, CANVAS_WIDTH, 1);
} }
// Draw border // Border
ctx.strokeStyle = dimColor; ctx.strokeStyle = `hsla(${h}, ${s}%, ${l}%, 0.4)`;
ctx.lineWidth = 1; ctx.lineWidth = 2;
ctx.strokeRect(0.5, 0.5, CANVAS_WIDTH - 1, CANVAS_HEIGHT - 1); 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 = () => { const handleMouseDown = () => {
setIsDragging(true); setIsDragging(true);
@ -191,23 +345,23 @@ const HumanVerification = ({ onVerified }: HumanVerificationProps) => {
}; };
const handleMouseUp = () => { const handleMouseUp = () => {
if (!isDragging || !puzzle || !trackRef.current) return; if (!isDragging || !trackRef.current) return;
setIsDragging(false); setIsDragging(false);
const maxSlide = trackRef.current.clientWidth - 48; const maxSlide = trackRef.current.clientWidth - 48;
const pieceX = (sliderX / maxSlide) * (CANVAS_WIDTH - PIECE_SIZE - 10) + 5; const pieceX = (sliderX / maxSlide) * (CANVAS_WIDTH - PIECE_WIDTH - NOTCH_SIZE - 10) + 5;
const diff = Math.abs(pieceX - puzzle.targetX); const diff = Math.abs(pieceX - targetX);
if (diff <= TOLERANCE) { if (diff <= TOLERANCE) {
setIsVerified(true); setIsVerified(true);
localStorage.setItem(VERIFIED_KEY, 'true'); localStorage.setItem(VERIFIED_KEY, 'true');
setTimeout(() => onVerified(), 600); setTimeout(() => onVerified(), 500);
} else { } else {
setError(true); setError(true);
setTimeout(() => { setTimeout(() => {
setSliderX(0); setSliderX(0);
setError(false); setError(false);
}, 400); }, 350);
} }
}; };
@ -229,40 +383,35 @@ const HumanVerification = ({ onVerified }: HumanVerificationProps) => {
<motion.div <motion.div
initial={{ opacity: 0, scale: 0.95 }} initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }} animate={{ opacity: 1, scale: 1 }}
className="w-full max-w-sm space-y-4" className="w-full max-w-[340px] space-y-3"
> >
{/* Header */} {/* 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"> <div className="text-primary font-mono text-sm tracking-wider font-bold">
SECURITY VERIFICATION v3.0 SECURITY VERIFICATION
</div> </div>
<div className="text-primary/60 font-mono text-xs"> <div className="text-primary/50 font-mono text-[10px]">
ANTI-BOT PROTOCOL ANTI-BOT PROTOCOL v3.0
</div> </div>
</div> </div>
{/* Instructions */}
<div className="text-primary font-mono text-sm">
{'>'} Slide the piece to complete the puzzle
</div>
{/* Canvas puzzle area */} {/* Canvas puzzle area */}
<div className="border border-primary/40 bg-background overflow-hidden relative"> <div className="border border-primary/30 overflow-hidden">
<canvas <canvas
ref={canvasRef} ref={canvasRef}
width={CANVAS_WIDTH} width={CANVAS_WIDTH}
height={CANVAS_HEIGHT} height={CANVAS_HEIGHT}
className="w-full" className="w-full block"
/> />
</div> </div>
{/* Slider track */} {/* Slider track */}
<div <div
ref={trackRef} ref={trackRef}
className={`relative h-12 border-2 ${ className={`relative h-11 border-2 ${
error ? 'border-destructive bg-destructive/10' : error ? 'border-destructive bg-destructive/10' :
isVerified ? 'border-green-500 bg-green-500/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`} } transition-colors duration-200 cursor-pointer select-none`}
onMouseMove={handleMouseMove} onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp} onMouseUp={handleMouseUp}
@ -272,10 +421,10 @@ const HumanVerification = ({ onVerified }: HumanVerificationProps) => {
> >
{/* Track label */} {/* Track label */}
<div className="absolute inset-0 flex items-center justify-center pointer-events-none"> <div className="absolute inset-0 flex items-center justify-center pointer-events-none">
<span className={`font-mono text-xs ${ <span className={`font-mono text-xs tracking-wide ${
isVerified ? 'text-green-500' : 'text-primary/40' isVerified ? 'text-green-500' : 'text-primary/30'
}`}> }`}>
{isVerified ? '[ ACCESS GRANTED ]' : '>>> SLIDE TO VERIFY >>>'} {isVerified ? '[ VERIFIED ]' : 'DRAG TO COMPLETE PUZZLE'}
</span> </span>
</div> </div>
@ -290,31 +439,25 @@ const HumanVerification = ({ onVerified }: HumanVerificationProps) => {
onMouseDown={handleMouseDown} onMouseDown={handleMouseDown}
onTouchStart={handleTouchStart} onTouchStart={handleTouchStart}
animate={error ? { x: [0, -4, 4, -4, 4, 0] } : {}} 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" /> <ChevronRight className="w-5 h-5 text-background" />
</motion.div> </motion.div>
</div> </div>
{/* Refresh */} {/* Controls */}
<div className="flex justify-between items-center text-xs font-mono text-muted-foreground"> <div className="flex justify-end">
<span>Protocol: SLIDER-VERIFY-3.0</span>
<button <button
onClick={generatePuzzle} 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" /> <RefreshCw className="w-3 h-3" />
Refresh Refresh
</button> </button>
</div> </div>
{/* Footer */}
<div className="text-center text-muted-foreground/50 font-mono text-xs">
// This verification helps protect against automated access
</div>
</motion.div> </motion.div>
</div> </div>
); );
}; };
export default HumanVerification; export default HumanVerification;

View File

@ -77,7 +77,7 @@ const Sidebar = () => {
</button> </button>
{/* Desktop Navigation - Always visible */} {/* 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) => { {navItems.map((item, index) => {
const isActive = location.pathname === item.path; 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 && ( {!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"> <p className="font-pixel text-sm text-primary text-glow text-center">
Access Granted Access Granted
</p> </p>

View File

@ -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>
);
};