diff --git a/src/components/AudioBlockedOverlay.tsx b/src/components/AudioBlockedOverlay.tsx new file mode 100644 index 0000000..ad95b97 --- /dev/null +++ b/src/components/AudioBlockedOverlay.tsx @@ -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 ( + +
+
+ +
+ +

+ AUDIO INITIALIZATION REQUIRED +

+ +

+ Browser security policy requires user interaction to enable audio playback. +

+ +
+ + + +
+ +

+ // Click anywhere or choose an option to continue +

+
+
+ ); +}; diff --git a/src/components/HumanVerification.tsx b/src/components/HumanVerification.tsx index 01b2cd4..83584f4 100644 --- a/src/components/HumanVerification.tsx +++ b/src/components/HumanVerification.tsx @@ -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(null); + const [puzzle, setPuzzle] = useState(null); + const [isDragging, setIsDragging] = useState(false); + const [sliderX, setSliderX] = useState(0); + const [isVerified, setIsVerified] = useState(false); + const [error, setError] = useState(false); + const trackRef = useRef(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 ( - -
- - {/* Header */} -
-
-{`┌─────────────────────────────────────┐
-│     SECURITY VERIFICATION v2.0      │
-│         ANTI-BOT PROTOCOL           │
-└─────────────────────────────────────┘`}
-            
-

- {'>'} Solve the equation to verify humanity -

+
+ + {/* Header */} +
+
+ SECURITY VERIFICATION v3.0
- - {/* Canvas with equation */} -
- +
+ ANTI-BOT PROTOCOL
- - {/* Input form */} -
-
- - {'>'}_ - - 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`} - /> -
- - - {error && ( - - - INCORRECT - {3 - attempts} attempts remaining - - )} - - -
- - -
-
- - {/* Footer */} -
-
- Protocol: MATH-VERIFY-2.0 - Encryption: ACTIVE -
-

- // This verification helps protect against automated access -

-
- - - {/* Terminal decoration */} -
-

{'>'} Awaiting verification input...

-

{'>'} _

-
- + + {/* Instructions */} +
+ {'>'} Slide the piece to complete the puzzle +
+ + {/* Canvas puzzle area */} +
+ +
+ + {/* Slider track */} +
+ {/* Track label */} +
+ + {isVerified ? '[ ACCESS GRANTED ]' : '>>> SLIDE TO VERIFY >>>'} + +
+ + {/* Slider handle */} + + + +
+ + {/* Refresh */} +
+ Protocol: SLIDER-VERIFY-3.0 + +
+ + {/* Footer */} +
+ // This verification helps protect against automated access +
+ +
); }; export default HumanVerification; -export { VERIFIED_KEY, BYPASS_KEY }; \ No newline at end of file diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index cfc3352..deb8c90 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -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(null); + const footerRef = useRef(null); + const sidebarRef = useRef(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 ( -