362 lines
16 KiB
TypeScript
362 lines
16 KiB
TypeScript
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
||
import './CybersecMenu.css';
|
||
|
||
interface Notification {
|
||
id: number;
|
||
title: string;
|
||
message: string;
|
||
removing: boolean;
|
||
}
|
||
|
||
const CybersecMenu: React.FC = () => {
|
||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||
const animFrameRef = useRef<number>(0);
|
||
const smokeOffsetRef = useRef(0);
|
||
const titleRef = useRef<HTMLHeadingElement>(null);
|
||
const [notification, setNotification] = useState<Notification | null>(null);
|
||
const notifIdRef = useRef(0);
|
||
const notifTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||
|
||
const showNotification = useCallback((title: string, message: string) => {
|
||
if (notifTimerRef.current) clearTimeout(notifTimerRef.current);
|
||
const id = ++notifIdRef.current;
|
||
setNotification({ id, title, message, removing: false });
|
||
notifTimerRef.current = setTimeout(() => {
|
||
setNotification(prev => prev?.id === id ? { ...prev, removing: true } : prev);
|
||
setTimeout(() => setNotification(prev => prev?.id === id ? null : prev), 300);
|
||
}, 3000);
|
||
}, []);
|
||
|
||
const selectLevel = useCallback((level: number | string) => {
|
||
const levelNames: Record<string | number, string> = {
|
||
1: 'Phishing – Fake emails & links',
|
||
2: 'Skimming – ATM/card fraud',
|
||
3: 'Password cracking – Brute force & dictionary',
|
||
4: 'Social engineering – Manipulation',
|
||
'deepfake': 'Voice deepfake – AI-generated speech',
|
||
};
|
||
showNotification(`>> LEVEL ${level} SELECTED`, levelNames[level]);
|
||
}, [showNotification]);
|
||
|
||
const openWiki = useCallback(() => {
|
||
showNotification('>> ACCESSING WIKI', 'Security knowledge base loading...');
|
||
}, [showNotification]);
|
||
|
||
const openQuiz = useCallback(() => {
|
||
showNotification('>> INITIATING QUIZ', 'Test your skills module loading...');
|
||
|
||
}, [showNotification]);
|
||
|
||
// Keyboard navigation
|
||
useEffect(() => {
|
||
const handleKey = (e: KeyboardEvent) => {
|
||
const map: Record<string, () => void> = {
|
||
'1': () => selectLevel(1),
|
||
'2': () => selectLevel(2),
|
||
'3': () => selectLevel(3),
|
||
'4': () => selectLevel(4),
|
||
'5': () => selectLevel('deepfake'),
|
||
'w': openWiki, 'W': openWiki,
|
||
'q': openQuiz, 'Q': openQuiz,
|
||
};
|
||
map[e.key]?.();
|
||
};
|
||
window.addEventListener('keydown', handleKey);
|
||
return () => window.removeEventListener('keydown', handleKey);
|
||
}, [selectLevel, openWiki, openQuiz]);
|
||
|
||
// Glitch effect on title
|
||
useEffect(() => {
|
||
const interval = setInterval(() => {
|
||
if (Math.random() > 0.9 && titleRef.current) {
|
||
const rx = (Math.random() * 10 - 5).toFixed(1);
|
||
const rx2 = (Math.random() * 10 - 5).toFixed(1);
|
||
titleRef.current.style.textShadow = `${rx}px 0 #FF0040, ${rx2}px 0 #00FFFF`;
|
||
setTimeout(() => {
|
||
if (titleRef.current) titleRef.current.style.textShadow = '0 0 30px rgba(0,255,255,0.5)';
|
||
}, 100);
|
||
}
|
||
}, 2000);
|
||
return () => clearInterval(interval);
|
||
}, []);
|
||
|
||
// Particle canvas
|
||
useEffect(() => {
|
||
const canvas = canvasRef.current;
|
||
if (!canvas) return;
|
||
const ctx = canvas.getContext('2d');
|
||
if (!ctx) return;
|
||
|
||
const resize = () => {
|
||
canvas.width = window.innerWidth;
|
||
canvas.height = window.innerHeight;
|
||
};
|
||
resize();
|
||
|
||
const COLORS = ['#00FF41', '#FF00FF', '#00FFFF', '#FF6600'];
|
||
|
||
class Particle {
|
||
x = 0; y = 0; size = 0; speedX = 0; speedY = 0;
|
||
opacity = 0; color = ''; life = 0; maxLife = 0;
|
||
constructor() { this.reset(); }
|
||
reset() {
|
||
this.x = Math.random() * canvas.width;
|
||
this.y = Math.random() * canvas.height;
|
||
this.size = Math.random() * 3 + 1;
|
||
this.speedX = (Math.random() - 0.5) * 0.5;
|
||
this.speedY = (Math.random() - 0.5) * 0.5;
|
||
this.opacity = Math.random() * 0.5 + 0.1;
|
||
this.color = COLORS[Math.floor(Math.random() * COLORS.length)];
|
||
this.life = 0;
|
||
this.maxLife = Math.random() * 200 + 100;
|
||
}
|
||
update() {
|
||
this.x += this.speedX;
|
||
this.y += this.speedY;
|
||
this.life++;
|
||
if (this.x < 0) this.x = canvas.width;
|
||
if (this.x > canvas.width) this.x = 0;
|
||
if (this.y < 0) this.y = canvas.height;
|
||
if (this.y > canvas.height) this.y = 0;
|
||
if (this.life > this.maxLife) this.reset();
|
||
}
|
||
draw() {
|
||
ctx.save();
|
||
ctx.globalAlpha = this.opacity * (1 - this.life / this.maxLife);
|
||
ctx.fillStyle = this.color;
|
||
ctx.shadowBlur = 10;
|
||
ctx.shadowColor = this.color;
|
||
ctx.beginPath();
|
||
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
ctx.restore();
|
||
}
|
||
}
|
||
|
||
const count = Math.min(80, Math.floor((canvas.width * canvas.height) / 15000));
|
||
let particles = Array.from({ length: count }, () => new Particle());
|
||
|
||
const onVis = () => {
|
||
if (document.hidden) { cancelAnimationFrame(animFrameRef.current); }
|
||
else { animate(); }
|
||
};
|
||
document.addEventListener('visibilitychange', onVis);
|
||
|
||
const onResize = () => { resize(); particles = Array.from({ length: Math.min(80, Math.floor((canvas.width * canvas.height) / 15000)) }, () => new Particle()); };
|
||
window.addEventListener('resize', onResize);
|
||
|
||
function drawSmoke() {
|
||
smokeOffsetRef.current += 0.2;
|
||
const so = smokeOffsetRef.current;
|
||
const layerColors = ['#00FF41', '#FF00FF', '#00FFFF'];
|
||
for (let layer = 0; layer < 3; layer++) {
|
||
ctx.save();
|
||
ctx.globalAlpha = 0.03 - layer * 0.008;
|
||
const grad = ctx.createRadialGradient(
|
||
canvas.width * 0.3 + Math.sin(so * 0.01 + layer) * 200,
|
||
canvas.height * 0.5 + Math.cos(so * 0.015 + layer) * 100,
|
||
0,
|
||
canvas.width * 0.5, canvas.height * 0.5,
|
||
canvas.width * 0.8
|
||
);
|
||
grad.addColorStop(0, layerColors[layer]);
|
||
grad.addColorStop(1, '#0a0a0a');
|
||
ctx.fillStyle = grad;
|
||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||
ctx.restore();
|
||
}
|
||
}
|
||
|
||
function animate() {
|
||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||
drawSmoke();
|
||
particles.forEach(p => { p.update(); p.draw(); });
|
||
ctx.save();
|
||
ctx.strokeStyle = 'rgba(0,255,255,0.05)';
|
||
ctx.lineWidth = 0.5;
|
||
for (let i = 0; i < particles.length; i++) {
|
||
for (let j = i + 1; j < particles.length; j++) {
|
||
const dx = particles[i].x - particles[j].x;
|
||
const dy = particles[i].y - particles[j].y;
|
||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||
if (dist < 100) {
|
||
ctx.globalAlpha = (1 - dist / 100) * 0.2;
|
||
ctx.beginPath();
|
||
ctx.moveTo(particles[i].x, particles[i].y);
|
||
ctx.lineTo(particles[j].x, particles[j].y);
|
||
ctx.stroke();
|
||
}
|
||
}
|
||
}
|
||
ctx.restore();
|
||
animFrameRef.current = requestAnimationFrame(animate);
|
||
}
|
||
|
||
animate();
|
||
return () => {
|
||
cancelAnimationFrame(animFrameRef.current);
|
||
window.removeEventListener('resize', onResize);
|
||
document.removeEventListener('visibilitychange', onVis);
|
||
};
|
||
}, []);
|
||
|
||
const menuItems = [
|
||
{ level: 1 as number | string, cls: 'csm-level-1', icon: '🔒', title: 'Level 1', desc: 'Phishing – Fake emails & links' },
|
||
{ level: 2 as number | string, cls: 'csm-level-2', icon: '⌨️', title: 'Level 2', desc: 'Skimming – ATM/card fraud' },
|
||
{ level: 3 as number | string, cls: 'csm-level-3', icon: '🖥️', title: 'Level 3', desc: 'Password cracking – Brute force & dictionary' },
|
||
{ level: 4 as number | string, cls: 'csm-level-4', icon: '🛡️', title: 'Level 4', desc: 'Social engineering – Manipulation' },
|
||
{ level: 'deepfake', cls: 'csm-deepfake', icon: '🎭', title: 'Deepfake', desc: 'Voice deepfake – AI-generated speech' },
|
||
];
|
||
|
||
return (
|
||
<div className="csm-root">
|
||
<canvas ref={canvasRef} id="csm-canvas" />
|
||
<div className="csm-grid-overlay" />
|
||
<div className="csm-scanlines" />
|
||
<div className="csm-noise" />
|
||
<div className="csm-corner csm-tl" />
|
||
<div className="csm-corner csm-tr" />
|
||
<div className="csm-corner csm-bl" />
|
||
<div className="csm-corner csm-br" />
|
||
|
||
<div className="csm-main-container">
|
||
{/* Header */}
|
||
<div className="csm-header">
|
||
<h1 ref={titleRef}>CYBERSEC TRAINING</h1>
|
||
<div className="csm-subtitle">[ TACTICAL SECURITY SIMULATION ]</div>
|
||
</div>
|
||
|
||
{/* Character */}
|
||
<div className="csm-character-container">
|
||
<svg className="csm-hacker-svg" viewBox="0 0 400 500" xmlns="http://www.w3.org/2000/svg">
|
||
<defs>
|
||
<linearGradient id="csm-hoodGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||
<stop offset="0%" style={{stopColor:'#1a1a1a',stopOpacity:1}}/>
|
||
<stop offset="100%" style={{stopColor:'#0d0d0d',stopOpacity:1}}/>
|
||
</linearGradient>
|
||
<linearGradient id="csm-maskGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||
<stop offset="0%" style={{stopColor:'#2a2a2a',stopOpacity:1}}/>
|
||
<stop offset="50%" style={{stopColor:'#1a1a1a',stopOpacity:1}}/>
|
||
<stop offset="100%" style={{stopColor:'#0a0a0a',stopOpacity:1}}/>
|
||
</linearGradient>
|
||
<filter id="csm-glow">
|
||
<feGaussianBlur stdDeviation="3" result="coloredBlur"/>
|
||
<feMerge><feMergeNode in="coloredBlur"/><feMergeNode in="SourceGraphic"/></feMerge>
|
||
</filter>
|
||
<filter id="csm-strongGlow">
|
||
<feGaussianBlur stdDeviation="5" result="coloredBlur"/>
|
||
<feMerge><feMergeNode in="coloredBlur"/><feMergeNode in="coloredBlur"/><feMergeNode in="SourceGraphic"/></feMerge>
|
||
</filter>
|
||
</defs>
|
||
|
||
{/* Hood/Head Silhouette */}
|
||
<path d="M200 80 C 150 80, 100 120, 90 180 C 85 210, 85 250, 95 290 C 100 320, 110 350, 130 370 L 130 420 C 130 440, 150 450, 200 450 C 250 450, 270 440, 270 420 L 270 370 C 290 350, 300 320, 305 290 C 315 250, 315 210, 310 180 C 300 120, 250 80, 200 80 Z"
|
||
fill="url(#csm-hoodGrad)" stroke="#00FF41" strokeWidth="2" filter="url(#csm-glow)"/>
|
||
{/* Hood Inner Shadow */}
|
||
<path d="M200 100 C 165 100, 130 130, 125 170 C 122 190, 122 220, 128 250 C 132 270, 140 290, 155 305 L 155 350 C 155 365, 170 375, 200 375 C 230 375, 245 365, 245 350 L 245 305 C 260 290, 268 270, 272 250 C 278 220, 278 190, 275 170 C 270 130, 235 100, 200 100 Z"
|
||
fill="#0a0a0a"/>
|
||
{/* Goggles/Mask */}
|
||
<rect x="125" y="200" width="150" height="70" rx="15"
|
||
fill="url(#csm-maskGrad)" stroke="#00FFFF" strokeWidth="2" filter="url(#csm-glow)"/>
|
||
{/* Goggle Lenses */}
|
||
<ellipse cx="165" cy="235" rx="28" ry="22" fill="#000" stroke="#00FFFF" strokeWidth="1.5"/>
|
||
<ellipse cx="235" cy="235" rx="28" ry="22" fill="#000" stroke="#00FFFF" strokeWidth="1.5"/>
|
||
{/* Glowing Eyes */}
|
||
<circle cx="165" cy="235" r="8" fill="#00FF41" className="csm-eye-glow" filter="url(#csm-strongGlow)"/>
|
||
<circle cx="235" cy="235" r="8" fill="#00FF41" className="csm-eye-glow" filter="url(#csm-strongGlow)"/>
|
||
{/* Eye Details */}
|
||
<circle cx="167" cy="233" r="3" fill="#fff" opacity="0.8"/>
|
||
<circle cx="237" cy="233" r="3" fill="#fff" opacity="0.8"/>
|
||
{/* Goggle Frame Details */}
|
||
<line x1="193" y1="220" x2="207" y2="220" stroke="#00FFFF" strokeWidth="2"/>
|
||
<line x1="193" y1="235" x2="207" y2="235" stroke="#00FFFF" strokeWidth="2"/>
|
||
<line x1="193" y1="250" x2="207" y2="250" stroke="#00FFFF" strokeWidth="2"/>
|
||
{/* Goggle Side Lights */}
|
||
<rect x="115" y="225" width="8" height="20" rx="2" fill="#FF00FF" filter="url(#csm-glow)" opacity="0.8"/>
|
||
<rect x="277" y="225" width="8" height="20" rx="2" fill="#FF00FF" filter="url(#csm-glow)" opacity="0.8"/>
|
||
{/* Shoulders/Torso */}
|
||
<path d="M80 380 C 60 390, 40 420, 30 480 L 30 500 L 370 500 L 370 480 C 360 420, 340 390, 320 380 C 280 360, 240 355, 200 355 C 160 355, 120 360, 80 380 Z"
|
||
fill="#0d0d0d" stroke="#00FF41" strokeWidth="1.5" opacity="0.6"/>
|
||
{/* Jacket Details */}
|
||
<path d="M200 355 L 200 500" stroke="#00FF41" strokeWidth="1" opacity="0.3"/>
|
||
<path d="M120 380 L 150 500" stroke="#00FF41" strokeWidth="1" opacity="0.2"/>
|
||
<path d="M280 380 L 250 500" stroke="#00FF41" strokeWidth="1" opacity="0.2"/>
|
||
{/* Collar */}
|
||
<path d="M150 340 C 150 340, 200 360, 250 340 L 260 370 C 260 370, 200 395, 140 370 Z"
|
||
fill="#1a1a1a" stroke="#00FFFF" strokeWidth="1"/>
|
||
{/* Holographic Tablet */}
|
||
<g transform="translate(220, 400) rotate(-10)">
|
||
<rect x="0" y="0" width="100" height="70" rx="5"
|
||
fill="rgba(0,20,40,0.9)" stroke="#00FF41" strokeWidth="2" className="csm-tablet-glow"/>
|
||
<rect x="5" y="5" width="90" height="60" rx="3" fill="rgba(0,10,20,0.95)"/>
|
||
<line x1="10" y1="12" x2="50" y2="12" stroke="#00FF41" strokeWidth="1.5" opacity="0.9"/>
|
||
<line x1="10" y1="20" x2="70" y2="20" stroke="#00FF41" strokeWidth="1.5" opacity="0.7"/>
|
||
<line x1="10" y1="28" x2="45" y2="28" stroke="#00FF41" strokeWidth="1.5" opacity="0.8"/>
|
||
<line x1="10" y1="36" x2="60" y2="36" stroke="#00FF41" strokeWidth="1.5" opacity="0.6"/>
|
||
<line x1="10" y1="44" x2="40" y2="44" stroke="#00FF41" strokeWidth="1.5" opacity="0.9"/>
|
||
<line x1="10" y1="52" x2="55" y2="52" stroke="#00FF41" strokeWidth="1.5" opacity="0.7"/>
|
||
<text x="55" y="58" className="csm-code-text" fontSize="6">01001</text>
|
||
<ellipse cx="50" cy="75" rx="45" ry="8" fill="url(#csm-hoodGrad)" opacity="0.3" filter="url(#csm-glow)"/>
|
||
</g>
|
||
{/* Hand */}
|
||
<ellipse cx="230" cy="445" rx="15" ry="12" fill="#1a1a1a" stroke="#00FF41" strokeWidth="1" opacity="0.8"/>
|
||
{/* Circuit Patterns */}
|
||
<path d="M120 150 L 140 150 L 145 160" stroke="#00FFFF" strokeWidth="0.5" fill="none" opacity="0.4"/>
|
||
<path d="M280 150 L 260 150 L 255 160" stroke="#00FFFF" strokeWidth="0.5" fill="none" opacity="0.4"/>
|
||
<circle cx="145" cy="160" r="2" fill="#00FFFF" opacity="0.5"/>
|
||
<circle cx="255" cy="160" r="2" fill="#00FFFF" opacity="0.5"/>
|
||
{/* Chin/Mouth */}
|
||
<path d="M175 290 C 175 290, 200 300, 225 290 L 225 305 C 225 305, 200 315, 175 305 Z"
|
||
fill="#0a0a0a" stroke="#00FF41" strokeWidth="0.5" opacity="0.5"/>
|
||
</svg>
|
||
</div>
|
||
|
||
{/* Right Side Menu */}
|
||
<div className="csm-right-menu">
|
||
{menuItems.map(item => (
|
||
<div key={String(item.level)} className={`csm-menu-item ${item.cls}`} onClick={() => selectLevel(item.level)}>
|
||
<div className="csm-hexagon">
|
||
<svg className="csm-hexagon-bg" viewBox="0 0 70 80">
|
||
<polygon points="35,2 66,20 66,58 35,76 4,58 4,20"/>
|
||
</svg>
|
||
<span className="csm-hexagon-icon">{item.icon}</span>
|
||
</div>
|
||
<div className="csm-tooltip">
|
||
<div className="csm-tooltip-title">{item.title}</div>
|
||
<div className="csm-tooltip-desc">{item.desc}</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
{/* Bottom Container */}
|
||
<div className="csm-bottom-container">
|
||
<div className="csm-bottom-block" onClick={openWiki}>
|
||
<div className="csm-block-shape">
|
||
<span className="csm-block-icon">📖</span>
|
||
<span className="csm-block-text">WIKI</span>
|
||
</div>
|
||
<div className="csm-block-tooltip">Security knowledge base</div>
|
||
</div>
|
||
<div className="csm-bottom-block" onClick={openQuiz}>
|
||
<div className="csm-block-shape">
|
||
<span className="csm-block-icon">❓</span>
|
||
<span className="csm-block-text">QUIZ</span>
|
||
</div>
|
||
<div className="csm-block-tooltip">Test your skills</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Notification */}
|
||
{notification && (
|
||
<div className={`csm-notification ${notification.removing ? 'csm-notif-out' : ''}`}>
|
||
<div className="csm-notif-title">{notification.title}</div>
|
||
<div className="csm-notif-message">{notification.message}</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default CybersecMenu;
|