mirror of
https://github.com/JorySeverijnse/ui-fixer-supreme.git
synced 2026-01-29 18:08:38 +00:00
Changes
This commit is contained in:
parent
21238dec45
commit
850630fbc6
@ -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;
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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