personal_website/src/components/TerminalCommand.tsx

209 lines
7.1 KiB
TypeScript

import { useState, useRef, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { motion, AnimatePresence } from 'framer-motion';
import { Terminal, X } from 'lucide-react';
import { useSettings } from '@/contexts/SettingsContext';
const commands: Record<string, string> = {
'/home': '/',
'/about': '/about',
'/me': '/about',
'/projects': '/projects',
'/proj': '/projects',
'/resources': '/resources',
'/res': '/resources',
'/links': '/links',
'/faq': '/faq',
'/games': '/games',
'/arcade': '/games',
'/tetris': '/games/tetris',
'/pacman': '/games/pacman',
'/snake': '/games/snake',
'/breakout': '/games/breakout',
'/music': '/music',
'/m': '/music',
'/ai': '/ai',
'/chat': '/ai',
};
const helpText = `Available commands:
/home - Navigate to Home
/about, /me - Navigate to About Me
/projects, /proj - Navigate to Projects
/resources, /res - Navigate to Resources
/links - Navigate to Links
/faq - Navigate to FAQ
/games, /arcade - Browse Arcade games
/tetris - Play Tetris
/pacman - Play Pac-Man
/snake - Play Snake
/breakout - Play Breakout
/music, /m - Navigate to Music Player
/ai, /chat - Navigate to AI Chat
/help, /h - Show this help message
/clear, /c - Clear terminal output`;
const TerminalCommand = () => {
const [isOpen, setIsOpen] = useState(false);
const [input, setInput] = useState('');
const [output, setOutput] = useState<string[]>(['Type /help for available commands']);
const inputRef = useRef<HTMLInputElement>(null);
const outputRef = useRef<HTMLDivElement>(null);
const navigate = useNavigate();
const { playSound } = useSettings();
useEffect(() => {
if (isOpen && inputRef.current) {
inputRef.current.focus();
}
}, [isOpen]);
useEffect(() => {
if (outputRef.current) {
outputRef.current.scrollTop = outputRef.current.scrollHeight;
}
}, [output]);
// Keyboard shortcut to toggle terminal
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// Don't trigger if already in an input field
const isInputFocused = document.activeElement?.tagName === 'INPUT' ||
document.activeElement?.tagName === 'TEXTAREA';
if (e.key === '`' || (e.ctrlKey && e.key === '/')) {
e.preventDefault();
setIsOpen(prev => !prev);
playSound('beep');
}
// Open terminal when pressing "/" (only if not in input)
if (e.key === '/' && !isOpen && !isInputFocused) {
e.preventDefault();
setIsOpen(true);
playSound('beep');
// Pre-fill with "/" so user can continue typing command
setTimeout(() => setInput('/'), 50);
}
if (e.key === 'Escape' && isOpen) {
setIsOpen(false);
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isOpen, playSound]);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const trimmedInput = input.trim().toLowerCase();
if (!trimmedInput) return;
playSound('click');
setOutput(prev => [...prev, `> ${input}`]);
if (trimmedInput === '/help' || trimmedInput === '/h') {
setOutput(prev => [...prev, helpText]);
} else if (trimmedInput === '/hint') {
setOutput(prev => [...prev,
'Hidden feature detected in system...',
'Hint: Old-school gamers know a certain cheat code.',
'Think NES, 1986, Contra... 30 lives anyone?',
'The sequence uses arrow keys and two letters.'
]);
} else if (trimmedInput === '/clear' || trimmedInput === '/c') {
setOutput(['Terminal cleared. Type /help for commands.']);
} else if (commands[trimmedInput]) {
setOutput(prev => [...prev, `Navigating to ${trimmedInput.slice(1)}...`]);
playSound('beep');
setTimeout(() => {
navigate(commands[trimmedInput]);
setIsOpen(false);
}, 300);
} else {
setOutput(prev => [...prev, `Command not found: ${trimmedInput}`, 'Type /help for available commands']);
}
setInput('');
};
return (
<>
{/* Terminal Toggle Button - aligned with music player */}
<motion.button
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.5 }}
onClick={() => {
setIsOpen(true);
playSound('beep');
}}
className="fixed bottom-4 right-4 z-[60] p-3 border-2 border-primary bg-background text-primary hover:bg-primary hover:text-background transition-all duration-300 box-glow"
title="Open Terminal (` or Ctrl+/)"
>
<Terminal className="w-5 h-5" />
</motion.button>
{/* Terminal Window */}
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0, y: 20, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 20, scale: 0.95 }}
className="fixed bottom-24 right-4 z-[200] w-[90vw] max-w-md border-2 border-primary bg-background/95 backdrop-blur-sm box-glow-strong"
>
{/* Terminal Header */}
<div className="flex items-center justify-between px-4 py-2 border-b border-primary bg-primary/10">
<div className="flex items-center gap-2">
<Terminal size={16} className="text-primary" />
<span className="font-minecraft text-sm text-primary text-glow">terminal@my-site.lol</span>
</div>
<button
onClick={() => setIsOpen(false)}
aria-label="Close terminal"
className="text-primary hover:text-primary/70 transition-colors"
>
<X size={16} />
</button>
</div>
{/* Terminal Output */}
<div
ref={outputRef}
className="h-48 overflow-y-auto p-4 font-mono text-sm text-primary/90"
>
{output.map((line, i) => (
<div key={i} className="whitespace-pre-wrap mb-1">
{line}
</div>
))}
</div>
{/* Terminal Input */}
<form onSubmit={handleSubmit} className="border-t border-primary/50">
<div className="flex items-center px-4 py-3">
<span className="text-primary font-mono mr-2">{'>'}</span>
<input
ref={inputRef}
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
className="flex-1 bg-transparent border-none outline-none font-mono text-primary placeholder-primary/40"
placeholder="Enter command..."
autoComplete="off"
spellCheck={false}
/>
<span className="text-primary animate-pulse">_</span>
</div>
</form>
</motion.div>
)}
</AnimatePresence>
</>
);
};
export default TerminalCommand;