mirror of
https://github.com/JorySeverijnse/ui-fixer-supreme.git
synced 2026-01-29 21:48:37 +00:00
Improve human verification UX
Enhance HumanVerification to a Taobao-style slider puzzle and ensure the Sidebar's Access Granted folds in when space is limited without making the sidebar scrollable. Includes refinements to keep the UI responsive on mobile and clarify that the access text folds rather than scrolls. X-Lovable-Edit-ID: edt-560d57af-660c-4447-8352-e12d8094d2c8
This commit is contained in:
commit
6b586236bc
@ -9,23 +9,22 @@ interface HumanVerificationProps {
|
|||||||
onVerified: () => void;
|
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 with glow
|
// Draw target slot
|
||||||
ctx.shadowColor = primaryColor;
|
ctx.save();
|
||||||
ctx.shadowBlur = 8;
|
drawPuzzlePiecePath(ctx, targetX, pieceY);
|
||||||
ctx.fillStyle = bgColor;
|
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
|
||||||
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();
|
||||||
|
|
||||||
// Inner slot pattern
|
// Calculate piece position from slider
|
||||||
ctx.shadowBlur = 0;
|
const maxSlide = trackRef.current ? trackRef.current.clientWidth - 48 : 240;
|
||||||
ctx.strokeStyle = `hsla(${h}, ${s}%, ${l}%, 0.2)`;
|
const pieceX = (sliderX / maxSlide) * (CANVAS_WIDTH - PIECE_WIDTH - NOTCH_SIZE - 10) + 5;
|
||||||
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
|
// Draw draggable piece with shadow/glow
|
||||||
const maxSlide = trackRef.current ? trackRef.current.clientWidth - 48 : 280;
|
ctx.save();
|
||||||
const pieceX = (sliderX / maxSlide) * (CANVAS_WIDTH - PIECE_SIZE - 10) + 5;
|
|
||||||
const pieceY = slotY;
|
let pieceColor = `hsla(${h}, ${s}%, ${l}%, 0.95)`;
|
||||||
|
let borderColor = `hsla(${h}, ${s}%, ${l * 1.2}%, 1)`;
|
||||||
|
let glowColor = `hsla(${h}, ${s}%, ${l}%, 0.6)`;
|
||||||
|
|
||||||
// Determine piece color
|
|
||||||
let pieceColor = primaryColor;
|
|
||||||
let pieceBorderColor = primaryColor;
|
|
||||||
if (error) {
|
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;
|
||||||
ctx.fillStyle = pieceColor;
|
|
||||||
ctx.strokeStyle = pieceBorderColor;
|
|
||||||
ctx.lineWidth = 2;
|
|
||||||
|
|
||||||
ctx.beginPath();
|
drawPuzzlePiecePath(ctx, pieceX, pieceY);
|
||||||
ctx.roundRect(pieceX, pieceY, PIECE_SIZE, PIECE_SIZE, 4);
|
ctx.fillStyle = pieceColor;
|
||||||
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,28 +439,22 @@ 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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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