juntekim.com/juntekim_frontend/app/components/TerminalBox.tsx
2025-12-07 14:07:28 +00:00

256 lines
7.6 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
vibecheck Check the vibe
clear Clears the screen
history Look into the past
exit You can't exit, it's a website
`,
pwd: () => "/home/juntekim/site",
whoami: () => "I suspect you know who you are.",
history: () => "If we keep looking back, we won't see the glory of now",
clear: () => "no takebacks, 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>
);
}