This commit is contained in:
gpt-engineer-app[bot]
2025-12-09 16:21:01 +00:00
parent 2a8700af31
commit 424e8f8277
7 changed files with 868 additions and 331 deletions
+55
View File
@@ -0,0 +1,55 @@
import { motion } from 'framer-motion';
import { Volume2, VolumeX } from 'lucide-react';
interface AudioBlockedOverlayProps {
onEnableAudio: () => void;
onDisableAudio: () => void;
}
export const AudioBlockedOverlay = ({ onEnableAudio, onDisableAudio }: AudioBlockedOverlayProps) => {
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="fixed inset-0 z-[9999] bg-background/95 backdrop-blur-sm flex items-center justify-center"
>
<div className="text-center space-y-6 p-8 max-w-md">
<div className="text-primary text-6xl mb-4">
<Volume2 className="w-16 h-16 mx-auto animate-pulse" />
</div>
<h2 className="text-2xl font-mono text-primary">
AUDIO INITIALIZATION REQUIRED
</h2>
<p className="text-muted-foreground font-mono text-sm">
Browser security policy requires user interaction to enable audio playback.
</p>
<div className="flex flex-col gap-3 mt-6">
<button
onClick={onEnableAudio}
className="w-full px-6 py-3 bg-primary/20 border border-primary text-primary font-mono
hover:bg-primary/30 transition-all duration-200 flex items-center justify-center gap-2"
>
<Volume2 className="w-5 h-5" />
ENABLE AUDIO
</button>
<button
onClick={onDisableAudio}
className="w-full px-6 py-3 bg-muted/20 border border-muted-foreground/30 text-muted-foreground font-mono
hover:bg-muted/30 transition-all duration-200 flex items-center justify-center gap-2"
>
<VolumeX className="w-5 h-5" />
DISABLE SOUNDS
</button>
</div>
<p className="text-muted-foreground/60 font-mono text-xs mt-4">
// Click anywhere or choose an option to continue
</p>
</div>
</motion.div>
);
};
+274 -218
View File
@@ -1,49 +1,33 @@
import { useState, useEffect, useRef } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { useState, useRef, useEffect, useCallback } from 'react';
import { motion } from 'framer-motion';
import { RefreshCw, ChevronRight } from 'lucide-react';
const BYPASS_KEY = 'bypass-human-check';
const VERIFIED_KEY = 'human-verified';
export const BYPASS_KEY = 'bypass-human-check';
export const VERIFIED_KEY = 'human-verified';
interface HumanVerificationProps {
onVerified: () => void;
}
// Generate a simple math equation
const generateEquation = () => {
const operators = ['+', '-', '*'] as const;
const operator = operators[Math.floor(Math.random() * operators.length)];
let a: number, b: number, answer: number;
switch (operator) {
case '+':
a = Math.floor(Math.random() * 50) + 1;
b = Math.floor(Math.random() * 50) + 1;
answer = a + b;
break;
case '-':
a = Math.floor(Math.random() * 50) + 20;
b = Math.floor(Math.random() * (a - 1)) + 1;
answer = a - b;
break;
case '*':
a = Math.floor(Math.random() * 12) + 2;
b = Math.floor(Math.random() * 12) + 2;
answer = a * b;
break;
}
return { equation: `${a} ${operator} ${b}`, answer };
};
interface PuzzlePiece {
targetX: number;
}
const HumanVerification = ({ onVerified }: HumanVerificationProps) => {
const [{ equation, answer }, setEquation] = useState(generateEquation);
const [userAnswer, setUserAnswer] = useState('');
const [error, setError] = useState(false);
const [attempts, setAttempts] = useState(0);
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 trackRef = useRef<HTMLDivElement>(null);
// Check for bypass in URL
const CANVAS_WIDTH = 320;
const CANVAS_HEIGHT = 160;
const PIECE_SIZE = 44;
const TOLERANCE = 10;
// Check for bypass in URL on mount
useEffect(() => {
const params = new URLSearchParams(window.location.search);
if (params.has(BYPASS_KEY) || window.location.pathname.includes(BYPASS_KEY)) {
@@ -52,213 +36,285 @@ const HumanVerification = ({ onVerified }: HumanVerificationProps) => {
}
}, [onVerified]);
// Draw equation on canvas for added security
const generatePuzzle = useCallback(() => {
const targetX = Math.floor(Math.random() * (CANVAS_WIDTH - PIECE_SIZE - 100)) + 80;
setPuzzle({ targetX });
setSliderX(0);
setError(false);
setIsVerified(false);
}, []);
useEffect(() => {
generatePuzzle();
}, [generatePuzzle]);
useEffect(() => {
if (!puzzle || !canvasRef.current) return;
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
// Get computed styles for theming
// Get theme color
const computedStyle = getComputedStyle(document.documentElement);
const primaryColor = computedStyle.getPropertyValue('--primary').trim();
const hslMatch = primaryColor.match(/[\d.]+/g);
const primaryRGB = hslMatch
? `hsl(${hslMatch[0]}, ${hslMatch[1]}%, ${hslMatch[2]}%)`
: '#00ff00';
// Clear canvas
ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw grid lines for terminal feel
ctx.strokeStyle = 'rgba(0, 255, 0, 0.1)';
ctx.lineWidth = 1;
for (let i = 0; i < canvas.width; i += 20) {
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;
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';
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) {
ctx.beginPath();
ctx.moveTo(i, 0);
ctx.lineTo(i, canvas.height);
ctx.moveTo(x, 0);
ctx.lineTo(x, CANVAS_HEIGHT);
ctx.stroke();
}
for (let i = 0; i < canvas.height; i += 20) {
for (let y = 0; y < CANVAS_HEIGHT; y += 16) {
ctx.beginPath();
ctx.moveTo(0, i);
ctx.lineTo(canvas.width, i);
ctx.moveTo(0, y);
ctx.lineTo(CANVAS_WIDTH, y);
ctx.stroke();
}
// Draw random matrix code in background
ctx.font = '10px monospace';
ctx.fillStyle = `hsla(${h}, ${s}%, ${l}%, 0.15)`;
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);
}
// Target slot position
const slotY = (CANVAS_HEIGHT - PIECE_SIZE) / 2;
// Draw border
ctx.strokeStyle = primaryRGB;
// Draw target slot with glow
ctx.shadowColor = primaryColor;
ctx.shadowBlur = 8;
ctx.fillStyle = bgColor;
ctx.strokeStyle = dimColor;
ctx.lineWidth = 2;
ctx.strokeRect(2, 2, canvas.width - 4, canvas.height - 4);
// Draw equation with slight random positioning for anti-bot
const offsetX = Math.random() * 10 - 5;
const offsetY = Math.random() * 6 - 3;
// Draw slot shape with puzzle notch
ctx.beginPath();
ctx.roundRect(puzzle.targetX, slotY, PIECE_SIZE, PIECE_SIZE, 4);
ctx.fill();
ctx.stroke();
ctx.font = 'bold 48px monospace';
ctx.fillStyle = primaryRGB;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// Add subtle noise/distortion
const chars = equation.split('');
let xPos = canvas.width / 2 - (chars.length * 15) + offsetX;
chars.forEach((char) => {
const yOffset = Math.random() * 4 - 2;
ctx.fillText(char, xPos, canvas.height / 2 + offsetY + yOffset);
xPos += 30;
});
// Draw "= ?" at the end
ctx.fillText('= ?', xPos + 20, canvas.height / 2 + offsetY);
// Add scanline effect
for (let y = 0; y < canvas.height; y += 3) {
ctx.fillStyle = 'rgba(0, 0, 0, 0.15)';
ctx.fillRect(0, y, canvas.width, 1);
}
}, [equation]);
// 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);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const parsed = parseInt(userAnswer, 10);
if (!isNaN(parsed) && parsed === answer) {
localStorage.setItem(VERIFIED_KEY, 'true');
onVerified();
} else {
setError(true);
setAttempts(prev => prev + 1);
setTimeout(() => {
setError(false);
// Generate new equation after 3 failed attempts
if (attempts >= 2) {
setEquation(generateEquation());
setAttempts(0);
}
}, 600);
setUserAnswer('');
}
};
// 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;
const handleNewEquation = () => {
setEquation(generateEquation());
setUserAnswer('');
// Determine piece color
let pieceColor = primaryColor;
let pieceBorderColor = primaryColor;
if (error) {
pieceColor = 'hsl(0, 70%, 50%)';
pieceBorderColor = 'hsl(0, 70%, 60%)';
} else if (isVerified) {
pieceColor = 'hsl(142, 76%, 36%)';
pieceBorderColor = 'hsl(142, 76%, 50%)';
}
// Draw puzzle piece with glow
ctx.shadowColor = pieceBorderColor;
ctx.shadowBlur = 12;
ctx.fillStyle = pieceColor;
ctx.strokeStyle = pieceBorderColor;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.roundRect(pieceX, pieceY, PIECE_SIZE, PIECE_SIZE, 4);
ctx.fill();
ctx.stroke();
ctx.shadowBlur = 0;
// Draw inner pattern on piece
ctx.strokeStyle = 'rgba(0, 0, 0, 0.4)';
ctx.lineWidth = 1;
ctx.strokeRect(pieceX + 8, pieceY + 8, PIECE_SIZE - 16, PIECE_SIZE - 16);
// Draw arrow on piece
ctx.fillStyle = 'rgba(0, 0, 0, 0.6)';
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);
ctx.closePath();
ctx.fill();
// Draw scanlines
ctx.fillStyle = 'rgba(0, 0, 0, 0.08)';
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);
}, [puzzle, sliderX, error, isVerified]);
const handleMouseDown = () => {
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 - 24, rect.width - 48));
setSliderX(x);
};
const handleMouseUp = () => {
if (!isDragging || !puzzle || !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);
if (diff <= TOLERANCE) {
setIsVerified(true);
localStorage.setItem(VERIFIED_KEY, 'true');
setTimeout(() => onVerified(), 600);
} else {
setError(true);
setTimeout(() => {
setSliderX(0);
setError(false);
}, 400);
}
};
const handleTouchStart = () => {
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 - 24, rect.width - 48));
setSliderX(x);
};
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="fixed inset-0 z-[100] bg-background flex items-center justify-center p-4"
>
<div className="max-w-lg w-full">
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ delay: 0.2 }}
className="border-2 border-primary box-glow p-6 bg-background"
>
{/* Header */}
<div className="text-center mb-6">
<pre className="font-mono text-[10px] sm:text-xs text-primary leading-tight mb-4">
{`┌─────────────────────────────────────┐
│ SECURITY VERIFICATION v2.0 │
│ ANTI-BOT PROTOCOL │
└─────────────────────────────────────┘`}
</pre>
<p className="font-mono text-sm text-foreground/70">
{'>'} Solve the equation to verify humanity
</p>
<div className="fixed inset-0 z-[100] bg-background flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
className="w-full max-w-sm space-y-4"
>
{/* Header */}
<div className="text-center border border-primary/40 bg-primary/5 py-3 px-4">
<div className="text-primary font-mono text-sm tracking-wider font-bold">
SECURITY VERIFICATION v3.0
</div>
{/* Canvas with equation */}
<div className="mb-6 flex justify-center">
<canvas
ref={canvasRef}
width={320}
height={100}
className="border border-primary/30 rounded"
/>
<div className="text-primary/60 font-mono text-xs">
ANTI-BOT PROTOCOL
</div>
{/* Input form */}
<form onSubmit={handleSubmit} className="space-y-4">
<div className="relative">
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-primary font-mono">
{'>'}_
</span>
<input
type="text"
inputMode="numeric"
pattern="-?[0-9]*"
value={userAnswer}
onChange={(e) => setUserAnswer(e.target.value.replace(/[^0-9-]/g, ''))}
placeholder="Enter answer"
autoFocus
className={`w-full bg-background border-2 ${
error ? 'border-destructive animate-pulse' : 'border-primary/50'
} p-3 pl-12 font-mono text-lg text-foreground placeholder:text-foreground/30 focus:outline-none focus:border-primary transition-colors`}
/>
</div>
<AnimatePresence>
{error && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
className="font-mono text-sm text-destructive flex items-center gap-2"
>
<span></span>
<span>INCORRECT - {3 - attempts} attempts remaining</span>
</motion.div>
)}
</AnimatePresence>
<div className="flex gap-3">
<button
type="submit"
disabled={!userAnswer}
className="flex-1 font-minecraft text-sm py-3 border-2 border-primary bg-primary/20 text-primary hover:bg-primary/40 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-300 box-glow"
>
[VERIFY]
</button>
<button
type="button"
onClick={handleNewEquation}
className="px-4 py-3 border-2 border-primary/50 text-primary/70 hover:border-primary hover:text-primary transition-all font-mono text-sm"
>
</button>
</div>
</form>
{/* Footer */}
<div className="mt-6 pt-4 border-t border-primary/20 space-y-2">
<div className="flex justify-between text-[10px] font-mono text-foreground/40">
<span>Protocol: MATH-VERIFY-2.0</span>
<span>Encryption: ACTIVE</span>
</div>
<p className="font-mono text-[10px] text-foreground/30 text-center">
// This verification helps protect against automated access
</p>
</div>
</motion.div>
{/* Terminal decoration */}
<div className="mt-4 font-mono text-[10px] text-primary/50 space-y-1">
<p>{'>'} Awaiting verification input...</p>
<p className="animate-pulse">{'>'} _</p>
</div>
</div>
</motion.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">
<canvas
ref={canvasRef}
width={CANVAS_WIDTH}
height={CANVAS_HEIGHT}
className="w-full"
/>
</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={handleMouseUp}
>
{/* 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'
}`}>
{isVerified ? '[ ACCESS GRANTED ]' : '>>> SLIDE TO VERIFY >>>'}
</span>
</div>
{/* Slider handle */}
<motion.div
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, -4, 4, -4, 4, 0] } : {}}
transition={{ duration: 0.25 }}
>
<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>
<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>
);
};
export default HumanVerification;
export { VERIFIED_KEY, BYPASS_KEY };
+47 -9
View File
@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useState, useRef, useEffect } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { motion, AnimatePresence } from 'framer-motion';
import { cn } from '@/lib/utils';
@@ -22,16 +22,43 @@ const Sidebar = () => {
const location = useLocation();
const { playSound } = useSettings();
const [isExpanded, setIsExpanded] = useState(false);
const [showFooter, setShowFooter] = useState(true);
const navRef = useRef<HTMLElement>(null);
const footerRef = useRef<HTMLDivElement>(null);
const sidebarRef = useRef<HTMLElement>(null);
const currentPage = navItems.find(item => item.path === location.pathname)?.label || 'Menu';
// Check if footer is visible within the sidebar
useEffect(() => {
const checkFooterVisibility = () => {
if (!sidebarRef.current || !footerRef.current || !navRef.current) return;
const sidebarRect = sidebarRef.current.getBoundingClientRect();
const navHeight = navRef.current.scrollHeight;
const footerHeight = footerRef.current.offsetHeight;
const availableHeight = sidebarRect.height;
// Calculate if there's enough space for nav items + footer
const totalContentHeight = navHeight + footerHeight + 32; // 32px for padding
setShowFooter(totalContentHeight <= availableHeight);
};
checkFooterVisibility();
window.addEventListener('resize', checkFooterVisibility);
return () => window.removeEventListener('resize', checkFooterVisibility);
}, []);
const toggleMenu = () => {
setIsExpanded(!isExpanded);
playSound('click');
};
return (
<aside className="w-full md:w-[230px] lg:w-[250px] h-auto md:h-full flex flex-col border-b-2 md:border-b-0 md:border-r-2 border-primary bg-background/70 box-glow shrink-0">
<aside
ref={sidebarRef}
className="w-full md:w-[230px] lg:w-[250px] h-auto md:h-full flex flex-col border-b-2 md:border-b-0 md:border-r-2 border-primary bg-background/70 box-glow shrink-0"
>
{/* Mobile Toggle Header */}
<button
onClick={toggleMenu}
@@ -50,7 +77,7 @@ const Sidebar = () => {
</button>
{/* Desktop Navigation - Always visible */}
<nav className="hidden md:block flex-grow overflow-hidden p-4 md:p-5">
<nav ref={navRef} className="hidden md:block flex-grow overflow-y-auto p-4 md:p-5">
{navItems.map((item, index) => {
const isActive = location.pathname === item.path;
@@ -80,6 +107,15 @@ const Sidebar = () => {
</motion.div>
);
})}
{/* Inline footer when sidebar is too short */}
{!showFooter && (
<div className="mt-4 pt-4 border-t border-primary/30">
<p className="font-pixel text-sm text-primary text-glow text-center">
Access Granted
</p>
</div>
)}
</nav>
{/* Mobile Navigation - Collapsible */}
@@ -127,12 +163,14 @@ const Sidebar = () => {
)}
</AnimatePresence>
{/* Desktop Footer */}
<div className="hidden md:block p-4 border-t border-primary/30">
<p className="font-pixel text-sm text-primary text-glow text-center">
Access Granted
</p>
</div>
{/* Desktop Footer - Only shown if visible */}
{showFooter && (
<div ref={footerRef} className="hidden md:block p-4 border-t border-primary/30">
<p className="font-pixel text-sm text-primary text-glow text-center">
Access Granted
</p>
</div>
)}
</aside>
);
};
+318
View File
@@ -0,0 +1,318 @@
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>
);
};