TypeScript · 7430 bytes Raw Blame History
1 import React, { useRef, useEffect } from 'react';
2
3 interface CommandHistoryEntry {
4 command: string;
5 output: string;
6 success: boolean;
7 }
8
9 interface TerminalProps {
10 commandHistory: CommandHistoryEntry[];
11 command: string;
12 setCommand: (cmd: string) => void;
13 executeCommand: (cmd: string) => void;
14 executing: boolean;
15 currentPath: string;
16 terminalMinimized: boolean;
17 setTerminalMinimized: (val: boolean) => void;
18 onGetCommandReference: () => void;
19 onGetHints: () => void;
20 onGetFHSReference: () => void;
21 }
22
23 const Terminal: React.FC<TerminalProps> = ({
24 commandHistory,
25 command,
26 setCommand,
27 executeCommand,
28 executing,
29 currentPath,
30 terminalMinimized,
31 setTerminalMinimized,
32 onGetCommandReference,
33 onGetHints,
34 onGetFHSReference,
35 }) => {
36 const terminalRef = useRef<HTMLDivElement>(null);
37 const inputRef = useRef<HTMLInputElement>(null);
38
39 // Auto-scroll terminal to bottom
40 useEffect(() => {
41 if (terminalRef.current) {
42 terminalRef.current.scrollTop = terminalRef.current.scrollHeight;
43 }
44 }, [commandHistory]);
45
46 // Auto-focus input after command execution
47 useEffect(() => {
48 if (!executing && inputRef.current && !terminalMinimized) {
49 inputRef.current.focus();
50 }
51 }, [executing, terminalMinimized]);
52
53 // Terminal color scheme - always dark mode
54 const terminalColors = {
55 frame: 'bg-stone-200 border-stone-300',
56 header: 'bg-stone-300 border-stone-400',
57 headerText: 'text-stone-900',
58 content: 'bg-black',
59 closeButton: 'text-stone-700 hover:text-stone-900'
60 };
61
62 return (
63 <div className={`absolute top-4 left-4 ${terminalColors.frame} rounded-lg shadow-2xl border transition-all duration-300 z-30 ${
64 terminalMinimized ? 'w-80' : 'w-[700px]'
65 }`}>
66 {/* Terminal Header */}
67 <div className={`flex items-center justify-between ${terminalColors.header} px-4 py-2 rounded-t-lg border-b`}>
68 <div className="flex items-center gap-2">
69 <div className="flex gap-1.5">
70 <button
71 onClick={onGetCommandReference}
72 className="w-3.5 h-3.5 bg-red-500 hover:bg-red-400 rounded-full flex items-center justify-center transition-colors relative"
73 title="Command Reference"
74 >
75 <span className="text-[8px] font-bold text-gray-900 absolute">×</span>
76 </button>
77 <button
78 onClick={onGetHints}
79 className="w-3.5 h-3.5 bg-yellow-500 hover:bg-yellow-400 rounded-full flex items-center justify-center transition-colors relative"
80 title="Get Hint"
81 >
82 <span className="text-[9px] font-bold text-gray-900 absolute">?</span>
83 </button>
84 <button
85 onClick={onGetFHSReference}
86 className="w-3.5 h-3.5 bg-green-500 hover:bg-green-400 rounded-full flex items-center justify-center transition-colors relative"
87 title="FHS Directory Reference"
88 >
89 <span className="text-[9px] font-bold text-gray-900 absolute">/</span>
90 </button>
91 </div>
92 <h3 className={`text-sm font-medium ${terminalColors.headerText} ml-2`}>bash</h3>
93 </div>
94 <button
95 onClick={() => setTerminalMinimized(!terminalMinimized)}
96 className={`${terminalColors.closeButton} transition`}
97 >
98 {terminalMinimized ? '▼' : '▲'}
99 </button>
100 </div>
101
102 {/* Terminal Content */}
103 {!terminalMinimized && (
104 <div
105 ref={terminalRef}
106 className={`${terminalColors.content} p-4 font-terminal text-base h-[300px] overflow-y-auto scrollbar-thin scrollbar-thumb-gray-700`}
107 onClick={() => inputRef.current?.focus()}
108 >
109 {commandHistory.map((entry, index) => (
110 <div key={index} className="mb-1">
111 <div className="flex items-start font-terminal">
112 <span className="text-green-400">groundskeeper@molehill</span>
113 <span className="text-gray-400 mx-1">::</span>
114 <span className="text-blue-400">{entry.command.startsWith('Hunt started!') ? '~' : currentPath}</span>
115 <span className="text-gray-400 ml-1">$</span>
116 <span className={`ml-2 ${entry.command.startsWith('Hunt started!') ? 'text-yellow-400' : 'text-gray-300'}`}>
117 {entry.command.startsWith('Hunt started!') ? '' : entry.command}
118 </span>
119 </div>
120 {entry.output && (
121 <div className={`${entry.success ? 'text-gray-300' : 'text-red-400'} ml-0 mt-1 font-terminal whitespace-pre-wrap`}>
122 {entry.output.split('\n').map((line, i) => {
123 // Special coloring for mole detection messages
124 let lineClass = '';
125 if (line.includes('New mole detected')) {
126 lineClass = 'text-yellow-400';
127 } else if (line.includes('⚠️')) {
128 // Timer warnings
129 if (line.includes('CRITICAL')) {
130 lineClass = 'text-red-500';
131 } else if (line.includes('ALERT')) {
132 lineClass = 'text-orange-400';
133 } else if (line.includes('WARNING')) {
134 lineClass = 'text-yellow-400';
135 }
136 }
137
138 return (
139 <div key={i} className={lineClass || ''}>
140 {line}
141 </div>
142 );
143 })}
144 </div>
145 )}
146 </div>
147 ))}
148
149 {/* Current input line */}
150 <div className="flex items-start font-terminal">
151 <span className="text-green-400">groundskeeper@molehill</span>
152 <span className="text-gray-400 mx-1">::</span>
153 <span className="text-blue-400">{currentPath}</span>
154 <span className="text-gray-400 ml-1">$</span>
155 <div className="flex-1 ml-2">
156 <div className="relative inline-block">
157 <span className="text-gray-300 font-terminal">{command}</span>
158 <span
159 className="text-gray-300 font-terminal"
160 style={{
161 animation: 'blink 1s step-end infinite'
162 }}
163 >
164 _
165 </span>
166 <input
167 ref={inputRef}
168 type="text"
169 value={command}
170 onChange={(e) => setCommand(e.target.value)}
171 onKeyDown={(e) => {
172 if (e.key === 'Enter') {
173 e.preventDefault();
174 executeCommand(command);
175 }
176 }}
177 disabled={executing}
178 className="absolute inset-0 w-full bg-transparent text-transparent outline-none caret-transparent font-terminal"
179 placeholder=""
180 autoFocus
181 spellCheck={false}
182 autoComplete="off"
183 autoCorrect="off"
184 autoCapitalize="off"
185 />
186 </div>
187 </div>
188 </div>
189 </div>
190 )}
191 </div>
192 );
193 };
194
195 export default Terminal;