juntekim.com/juntekim_frontend/app/components/TerminalBox.tsx
2025-12-07 13:50:40 +00:00

253 lines
7.5 KiB
TypeScript

"use client";
import { useState, useEffect, useRef } from "react";
import { QUOTES } from "@/lib/quotes";
export default function TerminalBox({ children }: { children: React.ReactNode }) {
// ---- BOOT SEQUENCE STATES ----
const [showFirstPrompt, setShowFirstPrompt] = useState(false);
const [showEnterEcho, setShowEnterEcho] = useState(false);
const [typedCommand1, setTypedCommand1] = useState("");
const [showQuote, setShowQuote] = useState(false);
const [quote, setQuote] = useState("");
const [typedCommand2, setTypedCommand2] = useState("");
const [showLoader, setShowLoader] = useState(false);
const [loaderProgress, setLoaderProgress] = useState(0);
const [showTree, setShowTree] = useState(false);
const [showIntroDone, setShowIntroDone] = useState(false);
// ---- INTERACTIVE STATES ----
const [history, setHistory] = useState<{ type: "input" | "output"; text: any }[]>([]);
const [currentInput, setCurrentInput] = useState("");
const inputRef = useRef<HTMLInputElement>(null);
// ---------------------------------------------------------
// BOOT SEQUENCE
// ---------------------------------------------------------
useEffect(() => {
setQuote(QUOTES[Math.floor(Math.random() * QUOTES.length)]);
setTimeout(() => setShowFirstPrompt(true), 400);
setTimeout(() => setShowEnterEcho(true), 1500);
setTimeout(() => typeCommand1(), 1800);
}, []);
// TYPE ONLY COMMAND, NOT PROMPT
const typeCommand1 = () => {
const cmd = "quoteOfTheDay";
let i = 0;
const interval = setInterval(() => {
setTypedCommand1(cmd.slice(0, i + 1));
i++;
if (i >= cmd.length) {
clearInterval(interval);
setTimeout(() => setShowQuote(true), 300);
setTimeout(() => typeCommand2(), 1200);
}
}, 50);
};
const typeCommand2 = () => {
const cmd = "tree .";
let i = 0;
const interval = setInterval(() => {
setTypedCommand2(cmd.slice(0, i + 1));
i++;
if (i >= cmd.length) {
clearInterval(interval);
setTimeout(() => startLoader(), 400);
}
}, 50);
};
const startLoader = () => {
setShowLoader(true);
let progress = 0;
const interval = setInterval(() => {
progress += Math.random() * 20;
if (progress >= 100) {
progress = 100;
clearInterval(interval);
setTimeout(() => {
setShowTree(true);
setShowLoader(false);
setShowIntroDone(true);
setTimeout(() => inputRef.current?.focus(), 200);
}, 500);
}
setLoaderProgress(Math.floor(progress));
}, 200);
};
// ---------------------------------------------------------
// COMMAND REGISTRY
// ---------------------------------------------------------
const commands: Record<string, (args: string[]) => any> = {
quoteOfTheDay: () => QUOTES[Math.floor(Math.random() * QUOTES.length)],
help: () => `
Available commands:
quoteOfTheDay Show a random quote
tree . Show the file structure
ls, ls -la Minimal directory listing
cd <path> Playful message about not leaving
pwd Print working directory
whoami Identify yourself
motivation Generate motivation (may crash)
vibecheck Check the vibe
clear I will not clear your history :)
exit You can't exit, it's a website
`,
pwd: () => "/home/juntekim/site",
whoami: () => "juntekim",
clear: () => "no ❤️ you need to face your past commands.",
motivation: () => [
"Compiling motivation…",
setTimeout(() => {
setHistory(h => [...h, { type: "output", text: "Segmentation fault (core dumped)" }]);
}, 500),
],
vibecheck: () => {
const vibes = [
"vibes immaculate ✨",
"vibes questionable 🧐",
"vibes under maintenance 🚧",
"vibes loading… 0%",
"vibes not found",
];
return vibes[Math.floor(Math.random() * vibes.length)];
},
exit: () => "bro it's a website. chill.",
ls: () => "nothing to see here 👀",
"ls -la": () => "nothing to see here 👀",
cd: () => "where you trying to go? everything you need is in tree .",
tree: () => [
"Compiling…",
setTimeout(() => {
setHistory(h => [...h, { type: "output", text: "/" }]);
setHistory(h => [...h, { type: "output", text: children }]);
}, 400),
],
};
// ---------------------------------------------------------
// COMMAND EXECUTION
// ---------------------------------------------------------
const handleCommand = (cmd: string) => {
const parts = cmd.trim().split(" ");
const base = parts[0];
const args = parts.slice(1);
setHistory(h => [...h, { type: "input", text: cmd }]);
if (cmd === "tree .") return commands["tree"](args);
if (commands[base]) {
const output = commands[base](args);
if (typeof output === "string") {
setHistory(h => [...h, { type: "output", text: output }]);
}
return;
}
if (base === "cd") {
setHistory(h => [...h, { type: "output", text: commands["cd"](args) }]);
return;
}
setHistory(h => [...h, { type: "output", text: `command not found: ${cmd}` }]);
};
const onKeyDown = (e: any) => {
if (e.key === "Enter") {
handleCommand(currentInput);
setCurrentInput("");
}
};
// ---------------------------------------------------------
// RENDER
// ---------------------------------------------------------
return (
<div
className="rounded-lg border border-zinc-700 bg-black text-zinc-100 p-4 font-mono text-sm shadow-xl w-full max-w-2xl"
style={{ whiteSpace: "pre-wrap" }}
>
{/* BOOT SEQUENCE */}
{showFirstPrompt && <div className="text-green-400">juntekim@site:~$</div>}
{showEnterEcho && <div className="text-green-400">juntekim@site:~$</div>}
{typedCommand1 && (
<div className="flex">
<span className="text-green-400">juntekim@site:~$&nbsp;</span>
<span className="text-zinc-300">{typedCommand1}</span>
</div>
)}
{showQuote && <div className="text-zinc-300 italic my-2">{quote}</div>}
{typedCommand2 && (
<div className="flex">
<span className="text-green-400">juntekim@site:~$&nbsp;</span>
<span className="text-zinc-300">{typedCommand2}</span>
</div>
)}
{showLoader && (
<div className="text-green-400 mt-2">compiling {loaderProgress}%</div>
)}
{showTree && (
<div className="text-zinc-300 mt-2">
/
{children}
</div>
)}
{/* INTERACTIVE MODE */}
{showIntroDone && (
<>
{history.map((item, idx) => (
<div key={idx} className="flex">
{item.type === "input" ? (
<>
<span className="text-green-400">juntekim@site:~$&nbsp;</span>
<span className="text-zinc-300">{item.text}</span>
</>
) : (
<span className="text-zinc-300">{item.text}</span>
)}
</div>
))}
<div className="flex items-center mt-1">
<span className="text-green-400">juntekim@site:~$&nbsp;</span>
<input
ref={inputRef}
value={currentInput}
onChange={(e) => setCurrentInput(e.target.value)}
onKeyDown={onKeyDown}
className="bg-black text-zinc-300 outline-none w-full"
autoFocus
/>
</div>
</>
)}
</div>
);
}