mirror of
https://github.com/JorySeverijnse/ui-fixer-supreme.git
synced 2026-01-29 17:58:38 +00:00
- Up/Down Arrow Navigation: Users can now press ↑/↓ to navigate through previously executed commands
- History Storage: Commands are stored in state and persist during the session
- Duplicate Prevention: Avoids adding the same command multiple times consecutively
- Reset on Typing: Manual typing resets the history navigation position
✅ Tab Completion
- Auto-complete: Pressing Tab completes partial commands that match available commands
- Single Match: If only one command matches, it completes the full command
- Multiple Matches: If multiple commands match, displays all possible completions in the terminal output
- Case-insensitive: Works regardless of input case
281 lines
10 KiB
TypeScript
281 lines
10 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';
|
|
import { useAchievements } from '@/contexts/AchievementsContext';
|
|
|
|
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',
|
|
'/achievements': '/achievements',
|
|
'/ach': '/achievements',
|
|
'/a': '/achievements',
|
|
'/credits': '/credits',
|
|
'/cred': '/credits',
|
|
'/oscilloscope': '/oscilloscope',
|
|
'/oscope': '/oscilloscope',
|
|
'/o': '/oscilloscope',
|
|
};
|
|
|
|
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
|
|
/achievements /a - View achievements
|
|
/oscilloscope /o - Audio oscilloscope
|
|
/credits /cred - View credits
|
|
/help, /h - Show this help message
|
|
/clear, /c - Clear terminal output`;
|
|
|
|
const helpHint = `
|
|
╔══════════════════════════════════════════╗
|
|
║ ...there may be other commands. ║
|
|
║ Perhaps a ꃅꀤꈤ꓄ to an easter egg exists? ║
|
|
╚══════════════════════════════════════════╝`;
|
|
|
|
const TerminalCommand = () => {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const [input, setInput] = useState('');
|
|
const [output, setOutput] = useState<string[]>(['Type /help for available commands']);
|
|
const [commandHistory, setCommandHistory] = useState<string[]>([]);
|
|
const [historyIndex, setHistoryIndex] = useState(-1);
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
const outputRef = useRef<HTMLDivElement>(null);
|
|
const navigate = useNavigate();
|
|
const { playSound } = useSettings();
|
|
const { unlockAchievement } = useAchievements();
|
|
|
|
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();
|
|
|
|
if (!trimmedInput) return;
|
|
|
|
playSound('click');
|
|
setOutput(prev => [...prev, `> ${input}`]);
|
|
|
|
// Add to command history (avoid duplicates of the last command)
|
|
setCommandHistory(prev => {
|
|
const newHistory = prev.filter(cmd => cmd !== trimmedInput);
|
|
return [...newHistory, trimmedInput];
|
|
});
|
|
setHistoryIndex(-1);
|
|
|
|
// Unlock terminal user achievement
|
|
unlockAchievement('terminal_user');
|
|
|
|
const lowerInput = trimmedInput.toLowerCase();
|
|
|
|
if (lowerInput === '/help' || lowerInput === '/h') {
|
|
setOutput(prev => [...prev, helpText, '---HINT---' + helpHint]);
|
|
} else if (lowerInput === '/hint') {
|
|
unlockAchievement('hint_seeker');
|
|
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 (lowerInput === '/clear' || lowerInput === '/c') {
|
|
setOutput(['Terminal cleared. Type /help for commands.']);
|
|
} else if (commands[lowerInput]) {
|
|
setOutput(prev => [...prev, `Navigating to ${lowerInput.slice(1)}...`]);
|
|
playSound('beep');
|
|
setTimeout(() => {
|
|
navigate(commands[lowerInput]);
|
|
setIsOpen(false);
|
|
}, 300);
|
|
} else {
|
|
setOutput(prev => [...prev, `Command not found: ${lowerInput}`, '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@fbi.gov</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.startsWith('---HINT---') ? 'text-yellow-400 animate-pulse' : ''}`}>
|
|
{line.startsWith('---HINT---') ? line.replace('---HINT---', '') : 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);
|
|
setHistoryIndex(-1); // Reset history navigation when typing
|
|
}}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'ArrowUp') {
|
|
e.preventDefault();
|
|
if (commandHistory.length > 0) {
|
|
const newIndex = historyIndex === -1 ? commandHistory.length - 1 : Math.max(0, historyIndex - 1);
|
|
setHistoryIndex(newIndex);
|
|
setInput(commandHistory[newIndex]);
|
|
}
|
|
} else if (e.key === 'ArrowDown') {
|
|
e.preventDefault();
|
|
if (historyIndex >= 0) {
|
|
const newIndex = historyIndex + 1;
|
|
if (newIndex >= commandHistory.length) {
|
|
setHistoryIndex(-1);
|
|
setInput('');
|
|
} else {
|
|
setHistoryIndex(newIndex);
|
|
setInput(commandHistory[newIndex]);
|
|
}
|
|
}
|
|
} else if (e.key === 'Tab') {
|
|
e.preventDefault();
|
|
const currentInput = input.trim().toLowerCase();
|
|
if (currentInput) {
|
|
// Find commands that start with current input
|
|
const matches = Object.keys(commands).filter(cmd => cmd.startsWith(currentInput));
|
|
if (matches.length === 1) {
|
|
setInput(matches[0]);
|
|
} else if (matches.length > 1) {
|
|
setOutput(prev => [...prev, `Possible completions: ${matches.join(', ')}`]);
|
|
}
|
|
}
|
|
}
|
|
}}
|
|
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;
|