mirror of
https://github.com/JorySeverijnse/ui-fixer-supreme.git
synced 2026-01-29 23:38:36 +00:00
219 lines
7.4 KiB
TypeScript
219 lines
7.4 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',
|
|
};
|
|
|
|
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
|
|
|
|
...there may be other commands.`;
|
|
|
|
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();
|
|
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().toLowerCase();
|
|
|
|
if (!trimmedInput) return;
|
|
|
|
playSound('click');
|
|
setOutput(prev => [...prev, `> ${input}`]);
|
|
|
|
// Unlock terminal user achievement
|
|
unlockAchievement('terminal_user');
|
|
|
|
if (trimmedInput === '/help' || trimmedInput === '/h') {
|
|
setOutput(prev => [...prev, helpText]);
|
|
} else if (trimmedInput === '/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 (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; |