Skip to content

Commit 9eccece

Browse files
committed
Fix terminal.
1 parent b6dc283 commit 9eccece

File tree

1 file changed

+109
-60
lines changed

1 file changed

+109
-60
lines changed
Lines changed: 109 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,119 +1,168 @@
1-
import { type FormEvent, useEffect, useRef, useState } from "react";
1+
"use client";
2+
3+
import { useTheme } from "next-themes";
4+
import { type ChangeEvent, type KeyboardEvent, useEffect, useRef, useState } from "react";
5+
6+
import responses from "./responses";
7+
8+
const macButtons = [
9+
{ color: "bg-red-500", label: "Close" },
10+
{ color: "bg-yellow-400", label: "Minimize" },
11+
{ color: "bg-green-500", label: "Zoom" },
12+
];
213

314
type HistoryItem =
4-
| { type: "input"; value: string }
515
| { type: "output"; value: string[] }
16+
| { type: "input"; value: string }
617
| { type: "prompt"; value: string };
718

19+
const initialHistory: HistoryItem[] = [
20+
{
21+
type: "output",
22+
value: ["Welcome to EternalCode CLI!", "Type 'help' to see available commands."],
23+
},
24+
];
25+
826
const themes = {
927
dark: {
10-
background: "bg-gray-900",
11-
text: "text-gray-100",
12-
promptText: "text-green-400",
13-
commandText: "text-blue-300",
14-
outputText: "text-gray-200",
28+
container: "bg-gradient-to-br from-gray-900 to-gray-800 border border-gray-700",
29+
titleBar: "bg-gradient-to-r from-gray-800 to-gray-700 border-b border-gray-700",
30+
titleText: "text-gray-400",
31+
terminalText: "text-gray-100",
32+
outputText: "text-gray-400",
1533
inputText: "text-gray-100",
34+
promptText: "text-green-400",
35+
commandText: "text-blue-400",
1636
},
1737
light: {
18-
background: "bg-gray-50",
19-
text: "text-gray-800",
38+
container: "bg-gradient-to-br from-gray-100 to-white border border-gray-300",
39+
titleBar: "bg-gradient-to-r from-gray-200 to-gray-100 border-b border-gray-300",
40+
titleText: "text-gray-700",
41+
terminalText: "text-gray-800",
42+
outputText: "text-gray-600",
43+
inputText: "text-gray-800",
2044
promptText: "text-green-600",
2145
commandText: "text-blue-600",
22-
outputText: "text-gray-700",
23-
inputText: "text-gray-900",
2446
},
2547
};
2648

27-
export default function Terminal({ theme = "dark" }: { theme?: keyof typeof themes }) {
28-
const currentTheme = themes[theme];
29-
const [history, setHistory] = useState<HistoryItem[]>([
30-
{ type: "prompt", value: "Welcome to EternalCode CLI" },
31-
]);
49+
export default function Terminal() {
50+
const [history, setHistory] = useState<HistoryItem[]>(initialHistory);
3251
const [input, setInput] = useState("");
33-
const endRef = useRef<HTMLDivElement | null>(null);
52+
const { resolvedTheme } = useTheme();
53+
const inputRef = useRef<HTMLInputElement>(null);
54+
const scrollRef = useRef<HTMLDivElement>(null);
55+
56+
useEffect(() => {
57+
scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight, behavior: "smooth" });
58+
}, []);
59+
60+
const handleInput = (e: ChangeEvent<HTMLInputElement>) => {
61+
setInput(e.target.value);
62+
};
63+
64+
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
65+
if (e.key === "Enter" && input.trim() !== "") {
66+
const fullCommand = input.trim();
67+
const commandParts = fullCommand.split(" ");
68+
const command = commandParts[0].toLowerCase();
69+
let output: string[] = [];
3470

35-
const handleCommand = async (command: string) => {
36-
setHistory((prev) => [...prev, { type: "input", value: command }]);
71+
if (command === "clear") {
72+
setHistory(initialHistory);
73+
setInput("");
74+
return;
75+
} else if (responses[command]) {
76+
output = responses[command];
77+
} else {
78+
output = [`eternalcode: command not found: ${input}`];
79+
}
3780

38-
if (command === "help") {
3981
setHistory((prev) => [
4082
...prev,
41-
{
42-
type: "output",
43-
value: [
44-
"Available commands:",
45-
"help - show available commands",
46-
"clear - clear the screen",
47-
],
48-
},
83+
{ type: "input", value: fullCommand } as HistoryItem,
84+
{ type: "output", value: output } as HistoryItem,
4985
]);
50-
} else if (command === "clear") {
51-
setHistory([]);
52-
} else {
53-
setHistory((prev) => [...prev, { type: "output", value: [`Unknown command: ${command}`] }]);
86+
setInput("");
5487
}
5588
};
5689

57-
const onSubmit = (e: FormEvent) => {
58-
e.preventDefault();
59-
if (!input.trim()) return;
60-
handleCommand(input.trim());
61-
setInput("");
62-
};
63-
64-
useEffect(() => {
65-
endRef.current?.scrollIntoView({ behavior: "smooth" });
66-
});
90+
const currentTheme = themes[resolvedTheme === "dark" ? "dark" : "light"];
6791

6892
return (
6993
<div
70-
className={`${currentTheme.background} ${currentTheme.text} p-4 rounded-lg font-mono text-sm`}
94+
className={`mx-auto mt-8 w-full max-w-2xl rounded-2xl shadow-2xl ${currentTheme.container}`}
7195
>
72-
<div className="space-y-1">
96+
{/* Title bar */}
97+
<div className={`flex h-10 items-center rounded-t-2xl px-4 ${currentTheme.titleBar}`}>
98+
<div className="flex space-x-2">
99+
{macButtons.map((btn) => (
100+
<span
101+
key={btn.label}
102+
className={`h-3 w-3 rounded-full ${btn.color} border border-black/20 shadow-inner`}
103+
title={btn.label}
104+
/>
105+
))}
106+
</div>
107+
<div
108+
className={`flex-1 select-none text-center font-mono text-xs tracking-wide ${currentTheme.titleText}`}
109+
>
110+
EternalCode Terminal
111+
</div>
112+
</div>
113+
{/* Terminal body */}
114+
<div
115+
ref={scrollRef}
116+
className={`h-96 overflow-y-auto bg-transparent px-6 py-4 font-mono text-sm ${currentTheme.terminalText}`}
117+
style={{ scrollbarWidth: "thin" }}
118+
>
73119
{history.map((item) => {
120+
const key = `${item.type}-${item.type === "output" ? item.value.join("-") : item.value}-${Date.now() + Math.random()}`;
121+
74122
if (item.type === "output") {
75123
return item.value.map((line) => (
76124
<div
77-
key={`output-${item.type}-${line}`}
125+
key={`${key}-${line}`}
78126
className={`${currentTheme.outputText} select-text whitespace-pre-line`}
79127
>
80128
{line}
81129
</div>
82130
));
83131
}
84-
85132
if (item.type === "input") {
86133
return (
87134
<div
88-
key={`input-${item.value}`}
135+
key={key}
89136
className={`${currentTheme.commandText} select-text whitespace-pre-line`}
90137
>
91138
eternalcode@cli:~$ <span className={currentTheme.inputText}>{item.value}</span>
92139
</div>
93140
);
94141
}
95-
96142
if (item.type === "prompt") {
97143
return (
98-
<div key={`prompt-${item.value}`} className={currentTheme.promptText}>
144+
<div key={key} className={currentTheme.promptText}>
99145
{item.value}
100146
</div>
101147
);
102148
}
103-
104149
return null;
105150
})}
106-
<div ref={endRef} />
107-
</div>
108151

109-
<form onSubmit={onSubmit} className="mt-2 flex">
110-
<span className={`${currentTheme.promptText} mr-2`}>$</span>
111-
<input
112-
value={input}
113-
onChange={(e) => setInput(e.target.value)}
114-
className={`flex-1 bg-transparent outline-none ${currentTheme.text}`}
115-
/>
116-
</form>
152+
{/* Active prompt */}
153+
<div className="mt-1 flex items-center">
154+
<span className={currentTheme.promptText}>eternalcode@cli:~$</span>
155+
<input
156+
ref={inputRef}
157+
className={`ml-2 flex-1 border-none bg-transparent font-mono text-sm outline-none ${currentTheme.inputText}`}
158+
value={input}
159+
onChange={handleInput}
160+
onKeyDown={handleKeyDown}
161+
spellCheck={false}
162+
aria-label="Terminal input"
163+
/>
164+
</div>
165+
</div>
117166
</div>
118167
);
119168
}

0 commit comments

Comments
 (0)