Compare commits

..

5 Commits

Author SHA1 Message Date
gpt-engineer-app[bot]
6b586236bc 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
2025-12-09 16:42:56 +00:00
gpt-engineer-app[bot]
850630fbc6 Changes 2025-12-09 16:42:55 +00:00
gpt-engineer-app[bot]
21238dec45 Improve mobile UI and audio UX
- Detect and fold inaccessible sidebar sections when not fully visible
- Require at least 1 minute on page for visit-based achievements
- Implement overlay to resolve blocked AudioContext with user gesture
- Replace human verification canvas with Taobao-like slider puzzle for bot resistance

X-Lovable-Edit-ID: edt-51cce867-67fb-4b5b-b99d-55b173652a90
2025-12-09 16:21:02 +00:00
gpt-engineer-app[bot]
424e8f8277 Changes 2025-12-09 16:21:01 +00:00
2a8700af31 removed p achievement text 2025-12-09 16:06:06 +01:00
7 changed files with 709 additions and 350 deletions

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

View File

@ -1,49 +1,32 @@
import { useState, useEffect, useRef } from 'react'; import { useState, useRef, useEffect, useCallback } from 'react';
import { motion, AnimatePresence } from 'framer-motion'; import { motion } from 'framer-motion';
import { RefreshCw, ChevronRight } from 'lucide-react';
const BYPASS_KEY = 'bypass-human-check'; export const BYPASS_KEY = 'bypass-human-check';
const VERIFIED_KEY = 'human-verified'; export const VERIFIED_KEY = 'human-verified';
interface HumanVerificationProps { interface HumanVerificationProps {
onVerified: () => void; 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 };
};
const HumanVerification = ({ onVerified }: HumanVerificationProps) => { 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 canvasRef = useRef<HTMLCanvasElement>(null);
const [isDragging, setIsDragging] = useState(false);
const [sliderX, setSliderX] = useState(0);
const [isVerified, setIsVerified] = useState(false);
const [error, setError] = useState(false);
const [puzzleImage, setPuzzleImage] = useState<ImageData | null>(null);
const [targetX, setTargetX] = useState(0);
const trackRef = useRef<HTMLDivElement>(null);
// Check for bypass in URL const CANVAS_WIDTH = 300;
const CANVAS_HEIGHT = 150;
const PIECE_WIDTH = 42;
const PIECE_HEIGHT = 42;
const NOTCH_SIZE = 10;
const TOLERANCE = 6;
// Check for bypass in URL on mount
useEffect(() => { useEffect(() => {
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
if (params.has(BYPASS_KEY) || window.location.pathname.includes(BYPASS_KEY)) { if (params.has(BYPASS_KEY) || window.location.pathname.includes(BYPASS_KEY)) {
@ -52,213 +35,429 @@ const HumanVerification = ({ onVerified }: HumanVerificationProps) => {
} }
}, [onVerified]); }, [onVerified]);
// Draw equation on canvas for added security const drawPuzzlePiecePath = (ctx: CanvasRenderingContext2D, x: number, y: number) => {
useEffect(() => { const w = PIECE_WIDTH;
const canvas = canvasRef.current; const h = PIECE_HEIGHT;
if (!canvas) return; const n = NOTCH_SIZE;
const ctx = canvas.getContext('2d');
if (!ctx) return;
// Get computed styles for theming
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) {
ctx.beginPath();
ctx.moveTo(i, 0);
ctx.lineTo(i, canvas.height);
ctx.stroke();
}
for (let i = 0; i < canvas.height; i += 20) {
ctx.beginPath();
ctx.moveTo(0, i);
ctx.lineTo(canvas.width, i);
ctx.stroke();
}
// Draw border
ctx.strokeStyle = primaryRGB;
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;
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]);
const handleSubmit = (e: React.FormEvent) => { ctx.beginPath();
e.preventDefault(); ctx.moveTo(x, y);
const parsed = parseInt(userAnswer, 10); // Top edge
ctx.lineTo(x + w, y);
if (!isNaN(parsed) && parsed === answer) { // Right edge with notch
localStorage.setItem(VERIFIED_KEY, 'true'); ctx.lineTo(x + w, y + h * 0.35);
onVerified(); ctx.arc(x + w + n * 0.5, y + h * 0.5, n, -Math.PI * 0.5, Math.PI * 0.5, false);
} else { ctx.lineTo(x + w, y + h);
setError(true); // Bottom edge
setAttempts(prev => prev + 1); ctx.lineTo(x, y + h);
setTimeout(() => { // Left edge
setError(false); ctx.lineTo(x, y);
// Generate new equation after 3 failed attempts ctx.closePath();
if (attempts >= 2) {
setEquation(generateEquation());
setAttempts(0);
}
}, 600);
setUserAnswer('');
}
}; };
const handleNewEquation = () => { const generatePuzzle = useCallback(() => {
setEquation(generateEquation()); const canvas = canvasRef.current;
setUserAnswer(''); 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);
setError(false);
setIsVerified(false);
}, []);
useEffect(() => {
generatePuzzle();
}, [generatePuzzle]);
// Draw the puzzle piece at current position
useEffect(() => {
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 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 and redraw background
ctx.fillStyle = '#0d0d0d';
ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
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);
// 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.moveTo(startX, startY);
ctx.lineTo(startX + 40, startY);
ctx.lineTo(startX + 40, startY + 30);
ctx.stroke();
}
// Matrix chars
ctx.font = '11px monospace';
ctx.fillStyle = `hsla(${h}, ${s}%, ${l}%, 0.2)`;
const chars = '01アイウエオ田由甲申';
for (let i = 0; i < 30; i++) {
ctx.fillText(chars[i % chars.length], (i * 17) % CANVAS_WIDTH, (i * 13) % CANVAS_HEIGHT);
}
const pieceY = (CANVAS_HEIGHT - PIECE_HEIGHT) / 2;
// Draw target slot
ctx.save();
drawPuzzlePiecePath(ctx, targetX, pieceY);
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
ctx.fill();
ctx.strokeStyle = `hsla(${h}, ${s}%, ${l}%, 0.5)`;
ctx.lineWidth = 1.5;
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)`;
if (error) {
pieceColor = 'hsla(0, 70%, 50%, 0.95)';
borderColor = 'hsl(0, 70%, 60%)';
glowColor = 'hsla(0, 70%, 50%, 0.6)';
} else if (isVerified) {
pieceColor = 'hsla(142, 76%, 40%, 0.95)';
borderColor = 'hsl(142, 76%, 55%)';
glowColor = 'hsla(142, 76%, 40%, 0.6)';
}
// Glow effect
ctx.shadowColor = glowColor;
ctx.shadowBlur = 12;
drawPuzzlePiecePath(ctx, pieceX, pieceY);
ctx.fillStyle = pieceColor;
ctx.fill();
ctx.strokeStyle = borderColor;
ctx.lineWidth = 2;
ctx.stroke();
ctx.shadowBlur = 0;
// Inner pattern on piece
ctx.strokeStyle = 'rgba(0, 0, 0, 0.3)';
ctx.lineWidth = 1;
ctx.strokeRect(pieceX + 6, pieceY + 6, PIECE_WIDTH - 12, PIECE_HEIGHT - 12);
// Arrow
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
ctx.beginPath();
const arrowX = pieceX + PIECE_WIDTH / 2;
const arrowY = pieceY + PIECE_HEIGHT / 2;
ctx.moveTo(arrowX - 5, arrowY - 4);
ctx.lineTo(arrowX + 5, arrowY);
ctx.lineTo(arrowX - 5, arrowY + 4);
ctx.closePath();
ctx.fill();
ctx.restore();
// Scanlines
ctx.fillStyle = 'rgba(0, 0, 0, 0.05)';
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);
}, [sliderX, error, isVerified, targetX, puzzleImage]);
useEffect(() => {
redrawWithPiece();
}, [sliderX, error, isVerified, redrawWithPiece]);
const handleMouseDown = () => {
setIsDragging(true);
setError(false); 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 || !trackRef.current) return;
setIsDragging(false);
const maxSlide = trackRef.current.clientWidth - 48;
const pieceX = (sliderX / maxSlide) * (CANVAS_WIDTH - PIECE_WIDTH - NOTCH_SIZE - 10) + 5;
const diff = Math.abs(pieceX - targetX);
if (diff <= TOLERANCE) {
setIsVerified(true);
localStorage.setItem(VERIFIED_KEY, 'true');
setTimeout(() => onVerified(), 500);
} else {
setError(true);
setTimeout(() => {
setSliderX(0);
setError(false);
}, 350);
}
};
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 ( return (
<motion.div <div className="fixed inset-0 z-[100] bg-background flex items-center justify-center p-4">
initial={{ opacity: 0 }} <motion.div
animate={{ opacity: 1 }} initial={{ opacity: 0, scale: 0.95 }}
className="fixed inset-0 z-[100] bg-background flex items-center justify-center p-4" animate={{ opacity: 1, scale: 1 }}
> className="w-full max-w-[340px] space-y-3"
<div className="max-w-lg w-full"> >
<motion.div {/* Header */}
initial={{ scale: 0.9, opacity: 0 }} <div className="text-center border border-primary/40 bg-primary/5 py-2.5 px-4">
animate={{ scale: 1, opacity: 1 }} <div className="text-primary font-mono text-sm tracking-wider font-bold">
transition={{ delay: 0.2 }} SECURITY VERIFICATION
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> </div>
<div className="text-primary/50 font-mono text-[10px]">
{/* Canvas with equation */} ANTI-BOT PROTOCOL v3.0
<div className="mb-6 flex justify-center">
<canvas
ref={canvasRef}
width={320}
height={100}
className="border border-primary/30 rounded"
/>
</div> </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>
</div>
</motion.div> {/* Canvas puzzle area */}
<div className="border border-primary/30 overflow-hidden">
<canvas
ref={canvasRef}
width={CANVAS_WIDTH}
height={CANVAS_HEIGHT}
className="w-full block"
/>
</div>
{/* Slider track */}
<div
ref={trackRef}
className={`relative h-11 border-2 ${
error ? 'border-destructive bg-destructive/10' :
isVerified ? 'border-green-500 bg-green-500/10' :
'border-primary/30 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 tracking-wide ${
isVerified ? 'text-green-500' : 'text-primary/30'
}`}>
{isVerified ? '[ VERIFIED ]' : 'DRAG TO COMPLETE PUZZLE'}
</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.2 }}
>
<ChevronRight className="w-5 h-5 text-background" />
</motion.div>
</div>
{/* Controls */}
<div className="flex justify-end">
<button
onClick={generatePuzzle}
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" />
Refresh
</button>
</div>
</motion.div>
</div>
); );
}; };
export default HumanVerification; export default HumanVerification;
export { VERIFIED_KEY, BYPASS_KEY };

View File

@ -1,4 +1,4 @@
import { useState } from 'react'; import { useState, useRef, useEffect } from 'react';
import { Link, useLocation } from 'react-router-dom'; import { Link, useLocation } from 'react-router-dom';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
@ -22,16 +22,43 @@ const Sidebar = () => {
const location = useLocation(); const location = useLocation();
const { playSound } = useSettings(); const { playSound } = useSettings();
const [isExpanded, setIsExpanded] = useState(false); 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'; 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 = () => { const toggleMenu = () => {
setIsExpanded(!isExpanded); setIsExpanded(!isExpanded);
playSound('click'); playSound('click');
}; };
return ( 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 */} {/* Mobile Toggle Header */}
<button <button
onClick={toggleMenu} onClick={toggleMenu}
@ -50,7 +77,7 @@ const Sidebar = () => {
</button> </button>
{/* Desktop Navigation - Always visible */} {/* Desktop Navigation - Always visible */}
<nav className="hidden md:block flex-grow overflow-hidden 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;
@ -80,6 +107,15 @@ const Sidebar = () => {
</motion.div> </motion.div>
); );
})} })}
{/* Inline footer when sidebar is too short - folded into nav */}
{!showFooter && (
<div className="mt-auto pt-4 border-t border-primary/30">
<p className="font-pixel text-sm text-primary text-glow text-center">
Access Granted
</p>
</div>
)}
</nav> </nav>
{/* Mobile Navigation - Collapsible */} {/* Mobile Navigation - Collapsible */}
@ -127,12 +163,14 @@ const Sidebar = () => {
)} )}
</AnimatePresence> </AnimatePresence>
{/* Desktop Footer */} {/* Desktop Footer - Only shown if visible */}
<div className="hidden md:block p-4 border-t border-primary/30"> {showFooter && (
<p className="font-pixel text-sm text-primary text-glow text-center"> <div ref={footerRef} className="hidden md:block p-4 border-t border-primary/30">
Access Granted <p className="font-pixel text-sm text-primary text-glow text-center">
</p> Access Granted
</div> </p>
</div>
)}
</aside> </aside>
); );
}; };

View File

@ -63,6 +63,25 @@ interface AchievementsContextType {
unlockMaxScore: () => void; unlockMaxScore: () => void;
} }
// Track time spent on each page
const PAGE_TIME_KEY = 'page-time-tracking';
const getPageTimeTracking = (): Record<string, number> => {
const saved = localStorage.getItem(PAGE_TIME_KEY);
return saved ? JSON.parse(saved) : {};
};
const updatePageTime = (path: string, seconds: number): void => {
const tracking = getPageTimeTracking();
tracking[path] = (tracking[path] || 0) + seconds;
localStorage.setItem(PAGE_TIME_KEY, JSON.stringify(tracking));
};
export const getPageTime = (path: string): number => {
const tracking = getPageTimeTracking();
return tracking[path] || 0;
};
const defaultAchievements: Achievement[] = [ const defaultAchievements: Achievement[] = [
// Discovery achievements // Discovery achievements
{ id: 'first_visit', name: 'Hello World', description: 'Visit the site for the first time', icon: '👋', unlocked: false }, { id: 'first_visit', name: 'Hello World', description: 'Visit the site for the first time', icon: '👋', unlocked: false },
@ -71,17 +90,17 @@ const defaultAchievements: Achievement[] = [
{ id: 'terminal_user', name: 'Terminal Jockey', description: 'Use the terminal command interface', icon: '💻', unlocked: false }, { id: 'terminal_user', name: 'Terminal Jockey', description: 'Use the terminal command interface', icon: '💻', unlocked: false },
{ id: 'hint_seeker', name: 'Hint Seeker', description: 'Ask for a hint in the terminal', icon: '🔍', unlocked: false, secret: true }, { id: 'hint_seeker', name: 'Hint Seeker', description: 'Ask for a hint in the terminal', icon: '🔍', unlocked: false, secret: true },
// Navigation achievements // Navigation achievements - now require 1 minute on page
{ id: 'home_visitor', name: 'Home Base', description: 'Visit the home page', icon: '🏠', unlocked: false }, { id: 'home_visitor', name: 'Home Base', description: 'Spend 1 minute on the home page', icon: '🏠', unlocked: false },
{ id: 'about_visitor', name: 'Getting Personal', description: 'Learn about the site owner', icon: '👤', unlocked: false }, { id: 'about_visitor', name: 'Getting Personal', description: 'Spend 1 minute learning about the owner', icon: '👤', unlocked: false },
{ id: 'projects_visitor', name: 'Project Explorer', description: 'Check out the projects', icon: '📁', unlocked: false }, { id: 'projects_visitor', name: 'Project Explorer', description: 'Spend 1 minute exploring projects', icon: '📁', unlocked: false },
{ id: 'resources_visitor', name: 'Resource Collector', description: 'Browse the resources page', icon: '📚', unlocked: false }, { id: 'resources_visitor', name: 'Resource Collector', description: 'Spend 1 minute browsing resources', icon: '📚', unlocked: false },
{ id: 'links_visitor', name: 'Link Crawler', description: 'Visit the links page', icon: '🔗', unlocked: false }, { id: 'links_visitor', name: 'Link Crawler', description: 'Spend 1 minute on the links page', icon: '🔗', unlocked: false },
{ id: 'faq_visitor', name: 'Question Everything', description: 'Read the FAQ', icon: '❓', unlocked: false }, { id: 'faq_visitor', name: 'Question Everything', description: 'Spend 1 minute reading the FAQ', icon: '❓', unlocked: false },
{ id: 'music_visitor', name: 'DJ Mode', description: 'Open the music player', icon: '🎵', unlocked: false }, { id: 'music_visitor', name: 'DJ Mode', description: 'Spend 1 minute in the music player', icon: '🎵', unlocked: false },
{ id: 'ai_visitor', name: 'AI Whisperer', description: 'Chat with the AI', icon: '🤖', unlocked: false }, { id: 'ai_visitor', name: 'AI Whisperer', description: 'Spend 1 minute chatting with AI', icon: '🤖', unlocked: false },
{ id: 'arcade_visitor', name: 'Arcade Enthusiast', description: 'Visit the arcade', icon: '🕹️', unlocked: false }, { id: 'arcade_visitor', name: 'Arcade Enthusiast', description: 'Spend 1 minute in the arcade', icon: '🕹️', unlocked: false },
{ id: 'all_pages', name: 'Completionist', description: 'Visit every page on the site', icon: '🗺️', unlocked: false, secret: true }, { id: 'all_pages', name: 'Completionist', description: 'Spend 1 minute on every page', icon: '🗺️', unlocked: false, secret: true },
// Time achievements // Time achievements
{ id: 'time_15min', name: 'Quick Visit', description: 'Spend 15 minutes on the site', icon: '⏱️', unlocked: false }, { id: 'time_15min', name: 'Quick Visit', description: 'Spend 15 minutes on the site', icon: '⏱️', unlocked: false },
@ -183,9 +202,12 @@ export const AchievementsProvider = ({ children }: { children: ReactNode }) => {
if (hour >= 5 && hour < 7) unlockAchievement('early_bird'); if (hour >= 5 && hour < 7) unlockAchievement('early_bird');
}, []); }, []);
// Track page visits // Track page visits and time spent
const [currentPath, setCurrentPath] = useState(location.pathname);
useEffect(() => { useEffect(() => {
const path = location.pathname; const path = location.pathname;
setCurrentPath(path);
setVisitedPages(prev => { setVisitedPages(prev => {
const newSet = new Set(prev); const newSet = new Set(prev);
@ -194,30 +216,42 @@ export const AchievementsProvider = ({ children }: { children: ReactNode }) => {
return newSet; return newSet;
}); });
// Page-specific achievements // Track time on page - increment every second
if (path === '/') unlockAchievement('home_visitor'); const interval = setInterval(() => {
if (path === '/about') unlockAchievement('about_visitor'); updatePageTime(path, 1);
if (path === '/projects') unlockAchievement('projects_visitor'); const pageTime = getPageTime(path);
if (path === '/resources') unlockAchievement('resources_visitor');
if (path === '/links') unlockAchievement('links_visitor'); // Unlock achievements after 60 seconds (1 minute) on page
if (path === '/faq') unlockAchievement('faq_visitor'); if (pageTime >= 60) {
if (path === '/music') unlockAchievement('music_visitor'); if (path === '/') unlockAchievement('home_visitor');
if (path === '/ai') unlockAchievement('ai_visitor'); if (path === '/about') unlockAchievement('about_visitor');
if (path === '/games') unlockAchievement('arcade_visitor'); if (path === '/projects') unlockAchievement('projects_visitor');
if (path === '/resources') unlockAchievement('resources_visitor');
if (path === '/links') unlockAchievement('links_visitor');
if (path === '/faq') unlockAchievement('faq_visitor');
if (path === '/music') unlockAchievement('music_visitor');
if (path === '/ai') unlockAchievement('ai_visitor');
if (path === '/games') unlockAchievement('arcade_visitor');
}
}, 1000);
// Immediate unlocks for games and detail pages
if (path.startsWith('/projects/')) unlockAchievement('project_detail'); if (path.startsWith('/projects/')) unlockAchievement('project_detail');
if (path === '/games/leaderboard') unlockAchievement('leaderboard_check'); if (path === '/games/leaderboard') unlockAchievement('leaderboard_check');
if (path === '/games/tetris') unlockAchievement('tetris_played'); if (path === '/games/tetris') unlockAchievement('tetris_played');
if (path === '/games/pacman') unlockAchievement('pacman_played'); if (path === '/games/pacman') unlockAchievement('pacman_played');
if (path === '/games/snake') unlockAchievement('snake_played'); if (path === '/games/snake') unlockAchievement('snake_played');
if (path === '/games/breakout') unlockAchievement('breakout_played'); if (path === '/games/breakout') unlockAchievement('breakout_played');
return () => clearInterval(interval);
}, [location.pathname]); }, [location.pathname]);
// Check all pages visited // Check all pages visited (1 min on each)
useEffect(() => { useEffect(() => {
const requiredPages = ['/', '/about', '/projects', '/resources', '/links', '/faq', '/music', '/ai', '/games']; const requiredPages = ['/', '/about', '/projects', '/resources', '/links', '/faq', '/music', '/ai', '/games'];
const allVisited = requiredPages.every(p => visitedPages.has(p)); const allVisitedLongEnough = requiredPages.every(p => getPageTime(p) >= 60);
if (allVisited) unlockAchievement('all_pages'); if (allVisitedLongEnough) unlockAchievement('all_pages');
}, [visitedPages]); }, [visitedPages, timeOnSite]); // Check periodically with timeOnSite
// Check all games played // Check all games played
useEffect(() => { useEffect(() => {

View File

@ -7,7 +7,7 @@ interface SettingsContextType {
setCrtEnabled: (enabled: boolean) => void; setCrtEnabled: (enabled: boolean) => void;
soundEnabled: boolean; soundEnabled: boolean;
setSoundEnabled: (enabled: boolean) => void; setSoundEnabled: (enabled: boolean) => void;
cryptoConsent: boolean | null; // null = never asked this session cryptoConsent: boolean | null;
setCryptoConsent: (consent: boolean) => void; setCryptoConsent: (consent: boolean) => void;
playSound: (type: SoundType) => void; playSound: (type: SoundType) => void;
hashrate: number; hashrate: number;
@ -17,7 +17,12 @@ interface SettingsContextType {
acceptedHashes: number; acceptedHashes: number;
setAcceptedHashes: (hashes: number) => void; setAcceptedHashes: (hashes: number) => void;
audioBlocked: boolean; audioBlocked: boolean;
resetAudioContext: () => void; showAudioOverlay: boolean;
setShowAudioOverlay: (show: boolean) => void;
enableAudio: () => void;
disableAudio: () => void;
userInteracted: boolean;
setUserInteracted: (interacted: boolean) => void;
} }
const SettingsContext = createContext<SettingsContextType | undefined>(undefined); const SettingsContext = createContext<SettingsContextType | undefined>(undefined);
@ -37,13 +42,12 @@ export const SettingsProvider = ({ children }: { children: ReactNode }) => {
// On session start, if user previously declined, we reset to null to re-prompt // On session start, if user previously declined, we reset to null to re-prompt
const [cryptoConsent, setCryptoConsentState] = useState<boolean | null>(() => { const [cryptoConsent, setCryptoConsentState] = useState<boolean | null>(() => {
const saved = localStorage.getItem('cryptoConsent'); const saved = localStorage.getItem('cryptoConsent');
if (saved === null) return null; // Never set if (saved === null) return null;
const parsed = JSON.parse(saved); const parsed = JSON.parse(saved);
// If declined (false), return null to re-prompt on new session // If declined (false), reset to null to re-prompt on new session
// We use sessionStorage to track if we've already shown the prompt this session
const sessionPrompted = sessionStorage.getItem('cryptoConsentPrompted'); const sessionPrompted = sessionStorage.getItem('cryptoConsentPrompted');
if (parsed === false && !sessionPrompted) { if (parsed === false && !sessionPrompted) {
return null; // Re-prompt return null;
} }
return parsed; return parsed;
}); });
@ -55,33 +59,97 @@ export const SettingsProvider = ({ children }: { children: ReactNode }) => {
}; };
const [audioBlocked, setAudioBlocked] = useState(false); const [audioBlocked, setAudioBlocked] = useState(false);
const [audioFailCount, setAudioFailCount] = useState(0); const [showAudioOverlay, setShowAudioOverlay] = useState(false);
const [userInteracted, setUserInteracted] = useState(false);
const [hashrate, setHashrate] = useState(0); const [hashrate, setHashrate] = useState(0);
const [totalHashes, setTotalHashes] = useState(0); const [totalHashes, setTotalHashes] = useState(0);
const [acceptedHashes, setAcceptedHashes] = useState(0); const [acceptedHashes, setAcceptedHashes] = useState(0);
// Single AudioContext instance, persisted across renders // Single AudioContext instance
const audioContextRef = useRef<AudioContext | null>(null); const audioContextRef = useRef<AudioContext | null>(null);
// Use ref to always have current soundEnabled value in callbacks
const soundEnabledRef = useRef(soundEnabled); const soundEnabledRef = useRef(soundEnabled);
useEffect(() => { useEffect(() => {
soundEnabledRef.current = soundEnabled; soundEnabledRef.current = soundEnabled;
}, [soundEnabled]); }, [soundEnabled]);
// Detect audio blocked and show overlay
useEffect(() => {
if (!soundEnabled) return;
// Check if we need to show the audio overlay
const checkAudioState = () => {
if (audioContextRef.current) {
if (audioContextRef.current.state === 'suspended' && !userInteracted) {
setAudioBlocked(true);
setShowAudioOverlay(true);
}
} else {
// Try to create AudioContext to check if it's blocked
try {
const testContext = new (window.AudioContext || (window as any).webkitAudioContext)();
if (testContext.state === 'suspended') {
setAudioBlocked(true);
setShowAudioOverlay(true);
}
audioContextRef.current = testContext;
} catch (e) {
console.warn('AudioContext creation failed:', e);
}
}
};
// Small delay to let page load
const timeout = setTimeout(checkAudioState, 500);
return () => clearTimeout(timeout);
}, [soundEnabled, userInteracted]);
// Get or create AudioContext // Get or create AudioContext
const getAudioContext = useCallback(() => { const getAudioContext = useCallback(() => {
if (!audioContextRef.current) { if (!audioContextRef.current) {
audioContextRef.current = new (window.AudioContext || (window as any).webkitAudioContext)(); audioContextRef.current = new (window.AudioContext || (window as any).webkitAudioContext)();
} }
// Resume if suspended (browser autoplay policy)
if (audioContextRef.current.state === 'suspended') { if (audioContextRef.current.state === 'suspended') {
audioContextRef.current.resume(); audioContextRef.current.resume().catch(() => {
setAudioBlocked(true);
if (soundEnabledRef.current && !userInteracted) {
setShowAudioOverlay(true);
}
});
} }
return audioContextRef.current; return audioContextRef.current;
}, [userInteracted]);
// Enable audio after user interaction
const enableAudio = useCallback(() => {
setUserInteracted(true);
setShowAudioOverlay(false);
if (audioContextRef.current) {
audioContextRef.current.resume().then(() => {
setAudioBlocked(false);
}).catch(console.warn);
} else {
try {
audioContextRef.current = new (window.AudioContext || (window as any).webkitAudioContext)();
audioContextRef.current.resume().then(() => {
setAudioBlocked(false);
});
} catch (e) {
console.warn('AudioContext creation failed:', e);
}
}
}, []);
// Disable audio
const disableAudio = useCallback(() => {
setUserInteracted(true);
setShowAudioOverlay(false);
setSoundEnabled(false);
setAudioBlocked(false);
}, []); }, []);
// Cleanup on unmount // Cleanup on unmount
@ -102,33 +170,18 @@ export const SettingsProvider = ({ children }: { children: ReactNode }) => {
localStorage.setItem('soundEnabled', JSON.stringify(soundEnabled)); localStorage.setItem('soundEnabled', JSON.stringify(soundEnabled));
}, [soundEnabled]); }, [soundEnabled]);
// Reset audio context to try again after user interaction
const resetAudioContext = useCallback(() => {
if (audioContextRef.current) {
audioContextRef.current.close();
audioContextRef.current = null;
}
setAudioBlocked(false);
setAudioFailCount(0);
}, []);
const playSound = useCallback((type: SoundType) => { const playSound = useCallback((type: SoundType) => {
// Use ref to ensure we always have the latest value if (!soundEnabledRef.current || audioBlocked) return;
if (!soundEnabledRef.current) return;
try { try {
const audioContext = getAudioContext(); const audioContext = getAudioContext();
// Check if context is blocked
if (audioContext.state === 'suspended') { if (audioContext.state === 'suspended') {
audioContext.resume().catch(() => { audioContext.resume().catch(() => {
setAudioFailCount(prev => { setAudioBlocked(true);
const newCount = prev + 1; if (!userInteracted) {
if (newCount >= 3) { setShowAudioOverlay(true);
setAudioBlocked(true); }
}
return newCount;
});
}); });
return; return;
} }
@ -185,22 +238,15 @@ export const SettingsProvider = ({ children }: { children: ReactNode }) => {
break; break;
} }
// Reset fail count on success // Successfully played - audio is working
if (audioFailCount > 0) { if (audioBlocked) {
setAudioFailCount(0);
setAudioBlocked(false); setAudioBlocked(false);
} }
} catch (e) { } catch (e) {
console.warn('Audio playback failed:', e); console.warn('Audio playback failed:', e);
setAudioFailCount(prev => { setAudioBlocked(true);
const newCount = prev + 1;
if (newCount >= 3) {
setAudioBlocked(true);
}
return newCount;
});
} }
}, [getAudioContext, audioFailCount]); }, [getAudioContext, audioBlocked, userInteracted]);
return ( return (
<SettingsContext.Provider <SettingsContext.Provider
@ -219,7 +265,12 @@ export const SettingsProvider = ({ children }: { children: ReactNode }) => {
acceptedHashes, acceptedHashes,
setAcceptedHashes, setAcceptedHashes,
audioBlocked, audioBlocked,
resetAudioContext, showAudioOverlay,
setShowAudioOverlay,
enableAudio,
disableAudio,
userInteracted,
setUserInteracted,
}} }}
> >
{children} {children}

View File

@ -47,9 +47,6 @@ const Achievements = () => {
<Trophy className="w-8 h-8" /> <Trophy className="w-8 h-8" />
ACHIEVEMENTS ACHIEVEMENTS
</h1> </h1>
<p className="text-primary/70 font-mono text-sm">
You found the secret achievements panel! Track your progress below.
</p>
</div> </div>
{/* Stats */} {/* Stats */}

View File

@ -9,6 +9,7 @@ import { useSettings } from '@/contexts/SettingsContext';
import { useAchievements } from '@/contexts/AchievementsContext'; import { useAchievements } from '@/contexts/AchievementsContext';
import { useKonamiCode } from '@/hooks/useKonamiCode'; import { useKonamiCode } from '@/hooks/useKonamiCode';
import { toast } from '@/hooks/use-toast'; import { toast } from '@/hooks/use-toast';
import { AudioBlockedOverlay } from '@/components/AudioBlockedOverlay';
// Lazy load conditionally rendered components to reduce initial bundle // Lazy load conditionally rendered components to reduce initial bundle
const HumanVerification = lazy(() => import('@/components/HumanVerification')); const HumanVerification = lazy(() => import('@/components/HumanVerification'));
@ -33,8 +34,17 @@ const Index = () => {
}); });
const [showConsentModal, setShowConsentModal] = useState(false); const [showConsentModal, setShowConsentModal] = useState(false);
const [konamiActive, setKonamiActive] = useState(false); const [konamiActive, setKonamiActive] = useState(false);
const [audioPromptDismissed, setAudioPromptDismissed] = useState(false);
const { crtEnabled, playSound, cryptoConsent, audioBlocked, resetAudioContext, setSoundEnabled } = useSettings(); const {
crtEnabled,
playSound,
cryptoConsent,
showAudioOverlay,
enableAudio,
disableAudio,
soundEnabled,
setSoundEnabled
} = useSettings();
const { unlockAchievement } = useAchievements(); const { unlockAchievement } = useAchievements();
const { activated: konamiActivated, reset: resetKonami } = useKonamiCode(); const { activated: konamiActivated, reset: resetKonami } = useKonamiCode();
const location = useLocation(); const location = useLocation();
@ -87,39 +97,6 @@ const Index = () => {
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, [cryptoConsent]); }, [cryptoConsent]);
// Show audio blocked prompt
useEffect(() => {
if (audioBlocked && !audioPromptDismissed) {
toast({
title: "Sound Effects Blocked",
description: "Click anywhere or disable sounds in settings",
action: (
<div className="flex gap-2">
<button
onClick={() => {
resetAudioContext();
setAudioPromptDismissed(true);
}}
className="px-2 py-1 text-xs border border-primary rounded hover:bg-primary/20"
>
Retry
</button>
<button
onClick={() => {
setSoundEnabled(false);
setAudioPromptDismissed(true);
}}
className="px-2 py-1 text-xs border border-primary rounded hover:bg-primary/20"
>
Disable
</button>
</div>
),
duration: 10000,
});
}
}, [audioBlocked, audioPromptDismissed, resetAudioContext, setSoundEnabled]);
useEffect(() => { useEffect(() => {
if (isRedTheme) { if (isRedTheme) {
document.documentElement.classList.add('red-theme'); document.documentElement.classList.add('red-theme');
@ -163,6 +140,14 @@ const Index = () => {
<MatrixRain color={isRedTheme ? '#FF0000' : '#00FF00'} /> <MatrixRain color={isRedTheme ? '#FF0000' : '#00FF00'} />
<LoadingScreen isLoading={isLoading} /> <LoadingScreen isLoading={isLoading} />
{/* Audio blocked overlay */}
{showAudioOverlay && soundEnabled && (
<AudioBlockedOverlay
onEnableAudio={enableAudio}
onDisableAudio={disableAudio}
/>
)}
{/* Moving scanline - only visible when CRT is enabled */} {/* Moving scanline - only visible when CRT is enabled */}
<div className="moving-scanline" /> <div className="moving-scanline" />