Initial commit (history reset)

This commit is contained in:
Ronnie 2025-08-20 20:41:33 -04:00
commit 173a4fc272
47 changed files with 7016 additions and 0 deletions

45
src/app/commands/MOTD.tsx Normal file
View file

@ -0,0 +1,45 @@
const motdCommand = () => {
return [
<div key="motd" className="font-mono">
<div className="text-orange-500">
{` _____ _ _ _ `}
{`\n | __ \\ (_|_) | | `}
{`\n | |__) |___ _ __ _ __ _ _ ___ __| | _____ __`}
{`\n | _ // _ \\| '_ \\| '_ \\| | |/ _ \\ / _\` |/ _ \\ \\ / /`}
{`\n | | \\ \\ (_) | | | | | | | | | __/| (_| | __/\\ V / `}
{`\n |_| \\_\\___/|_| |_|_| |_|_|_|\\___(_)__,_|\\___| \\_/ `}
</div>
<div className="text-orange-400 mt-4">
<strong>Welcome to Ronnie&#39;s Project Terminal!</strong>
</div>
<div className="border-t border-gray-700 my-4"/>
<strong className="text-orange-500">🚀 ABOUT THIS TERMINAL</strong>
<p className="text-gray-400">
This is a showcase of my experimental projects hosted on *.ronniie.dev.
</p>
<p className="text-gray-400">
Each project is a unique experiment, from fun visualizations to useful tools.
</p>
<p className="text-gray-400">
Feel free to explore and interact with them!
</p>
<div className="border-t border-gray-700 my-4"/>
<p className="text-orange-400">
🌟 &#34;Every project is an adventure in learning and creation.&#34; 🌟
</p>
<div className="border-t border-gray-700 my-4"/>
<div className="text-gray-400 mt-2">
Type <span className="text-orange-500">&#39;help&#39;</span> to see available commands. Let&#39;s explore
together!
</div>
<div className="text-gray-400 mt-2">
Want to learn more about me? Visit <a href="https://ronniie.com" target="_blank" rel="noopener noreferrer" className="text-orange-400 hover:text-orange-300">ronniie.com</a> for my full portfolio and blog.
</div>
</div>
];
};
export default motdCommand;

View file

@ -0,0 +1,15 @@
import { Command } from "../../types/command";
const clearCommand: Command = {
metadata: {
name: "clear",
description: "Clear the terminal screen",
icon: "🧹",
},
execute: () => {
// The actual clearing is handled in the Terminal component
return [];
},
};
export default clearCommand;

28
src/app/commands/date.tsx Normal file
View file

@ -0,0 +1,28 @@
import { Command } from "../../types/command";
const dateCommand: Command = {
metadata: {
name: "date",
description: "Display the current date and time",
icon: "📅",
},
execute: () => {
const now = new Date();
return [
<div key="date" className="text-gray-400">
{now.toLocaleString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
timeZoneName: 'short'
})}
</div>
];
},
};
export default dateCommand;

29
src/app/commands/echo.tsx Normal file
View file

@ -0,0 +1,29 @@
import { Command } from "../../types/command";
const echoCommand: Command = {
metadata: {
name: "echo",
description: "Print text to the terminal",
icon: "📢",
usage: "echo <text>",
},
execute: (args: string[] = []) => {
if (args.length === 0) {
return [
<div key="echo-error" className="text-red-500">
Error: Missing arguments
<div className="text-gray-400 mt-1">
Usage: echo &lt;text&gt;
</div>
</div>
];
}
return [
<div key="echo" className="text-gray-400">
{args.join(" ")}
</div>
];
},
};
export default echoCommand;

20
src/app/commands/exit.tsx Normal file
View file

@ -0,0 +1,20 @@
import { Command } from "../../types/command";
const exitCommand: Command = {
metadata: {
name: "exit",
description: "Close the terminal",
icon: "❌",
},
execute: () => {
// This is a special case where we need to handle the exit command differently
// The actual execution is handled in the Terminal component
return [
<div key="exit" className="text-red-500">
Terminal exited. Refresh the page to restart. 🚀
</div>,
];
},
};
export default exitCommand;

104
src/app/commands/help.tsx Normal file
View file

@ -0,0 +1,104 @@
import { Command } from "../../types/command";
const helpCommand: Command = {
metadata: {
name: "help",
description: "Show available commands",
icon: "❓",
},
execute: () => {
return [
<div key="help" className="text-gray-400">
<div className="mb-6">
<h2 className="text-xl font-bold text-orange-500 mb-2">Available Commands</h2>
<p className="text-gray-400">Type any command to execute it. Use and to navigate history.</p>
</div>
<div className="space-y-6">
{/* System Commands */}
<div>
<h3 className="text-lg font-semibold text-orange-500 mb-2">System</h3>
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<span className="text-orange-500">clear</span>
<span className="text-gray-400">- Clear the terminal screen</span>
</div>
<div className="flex items-center gap-2">
<span className="text-orange-500">date</span>
<span className="text-gray-400">- Display current date and time</span>
</div>
<div className="flex items-center gap-2">
<span className="text-orange-500">echo</span>
<span className="text-gray-400">- Print text to the terminal</span>
</div>
<div className="text-gray-500 text-sm ml-6">Usage: echo &lt;text&gt;</div>
<div className="flex items-center gap-2">
<span className="text-orange-500">exit</span>
<span className="text-gray-400">- Exit the terminal</span>
</div>
<div className="flex items-center gap-2">
<span className="text-orange-500">help</span>
<span className="text-gray-400">- Show this help message</span>
</div>
<div className="flex items-center gap-2">
<span className="text-orange-500">neofetch</span>
<span className="text-gray-400">- Display system information</span>
</div>
<div className="flex items-center gap-2">
<span className="text-orange-500">whoami</span>
<span className="text-gray-400">- Show current user</span>
</div>
</div>
</div>
{/* File System Commands */}
<div>
<h3 className="text-lg font-semibold text-orange-500 mb-2">File System</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<span className="text-orange-500">ls</span>
<span className="text-gray-400">- List files and directories</span>
</div>
</div>
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<span className="text-orange-500">pwd</span>
<span className="text-gray-400">- Show current directory</span>
</div>
</div>
</div>
</div>
{/* Project Commands */}
<div>
<h3 className="text-lg font-semibold text-orange-500 mb-2">Projects</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<span className="text-orange-500">projects</span>
<span className="text-gray-400">- Show my projects</span>
</div>
<div className="text-gray-500 text-sm ml-6">Usage: projects [project-name]</div>
</div>
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<span className="text-orange-500">source</span>
<span className="text-gray-400">- View source code</span>
</div>
</div>
</div>
</div>
</div>
<div className="mt-6 pt-4 border-t border-gray-700">
<p className="text-gray-400">
💡 Pro tip: Commands are case-insensitive and support tab completion.
</p>
</div>
</div>
];
},
};
export default helpCommand;

26
src/app/commands/index.ts Normal file
View file

@ -0,0 +1,26 @@
import { Command } from "../../types/command";
import helpCommand from "./help";
import exitCommand from "./exit";
import sourceCommand from "./source";
import projectsCommand from "./projects";
import clearCommand from "./clear";
import dateCommand from "./date";
import neofetchCommand from "./neofetch";
import echoCommand from "./echo";
import whoamiCommand from "./whoami";
import pwdCommand from "./pwd";
import lsCommand from "./ls";
export const commands: Record<string, Command> = {
help: helpCommand,
exit: exitCommand,
source: sourceCommand,
projects: projectsCommand,
clear: clearCommand,
date: dateCommand,
neofetch: neofetchCommand,
echo: echoCommand,
whoami: whoamiCommand,
pwd: pwdCommand,
ls: lsCommand,
};

26
src/app/commands/ls.tsx Normal file
View file

@ -0,0 +1,26 @@
import { Command } from "../../types/command";
import projects from "../../data/projects.json";
const lsCommand: Command = {
metadata: {
name: "ls",
description: "List files and directories",
icon: "📋",
},
execute: () => {
return [
<div key="ls" className="text-gray-400">
<div className="grid grid-cols-1 gap-2">
{projects.projects.map((project) => (
<div key={project.name} className="flex items-center gap-2">
<span className="text-orange-500">📁</span>
<span>{project.name}</span>
</div>
))}
</div>
</div>
];
},
};
export default lsCommand;

View file

@ -0,0 +1,133 @@
import { Command, CommandContext } from "../../types/command";
// Add type declarations for Navigator properties
declare global {
interface Navigator {
deviceMemory?: number;
hardwareConcurrency?: number;
platform: string;
userAgent: string;
language: string;
vendor: string;
}
}
const neofetchCommand: Command = {
metadata: {
name: "neofetch",
description: "Display system information",
icon: "🖥️",
},
execute: (_, context?: CommandContext) => {
const platform = navigator.platform;
const userAgent = navigator.userAgent;
const language = navigator.language;
const vendor = navigator.vendor;
const memory = navigator.deviceMemory ? `${navigator.deviceMemory} GB` : "Unknown";
const cores = navigator.hardwareConcurrency || "Unknown";
const osInfo = {
platform: platform,
userAgent: userAgent,
language: language,
vendor: vendor,
memory: memory,
cores: cores,
};
// Enhanced OS detection
let os = 'Unknown';
let osVersion = '';
if (osInfo.userAgent.includes('Windows')) {
os = 'Windows';
if (osInfo.userAgent.includes('Windows NT 10.0')) {
// Windows 11 has a specific pattern in the user agent
// It includes "Windows NT 10.0" followed by specific identifiers
const isWindows11 = osInfo.userAgent.includes('Windows NT 10.0; Win64; x64') &&
(osInfo.userAgent.includes('Windows NT 10.0; Win64; x64; Windows NT 10.0') ||
osInfo.userAgent.includes('Windows NT 10.0; Win64; x64; rv:') ||
osInfo.userAgent.includes('Windows NT 10.0; Win64; x64; Edge/'));
osVersion = isWindows11 ? '11' : '10';
}
else if (osInfo.userAgent.includes('Windows NT 6.3')) osVersion = '8.1';
else if (osInfo.userAgent.includes('Windows NT 6.2')) osVersion = '8';
else if (osInfo.userAgent.includes('Windows NT 6.1')) osVersion = '7';
}
else if (osInfo.userAgent.includes('Mac')) {
os = 'macOS';
const match = osInfo.userAgent.match(/Mac OS X (\d+[._]\d+)/);
if (match) osVersion = match[1].replace('_', '.');
}
else if (osInfo.userAgent.includes('Linux')) {
os = 'Linux';
if (osInfo.userAgent.includes('Ubuntu')) osVersion = 'Ubuntu';
else if (osInfo.userAgent.includes('Fedora')) osVersion = 'Fedora';
else if (osInfo.userAgent.includes('Debian')) osVersion = 'Debian';
else if (osInfo.userAgent.includes('Android')) {
os = 'Android';
const match = osInfo.userAgent.match(/Android (\d+[._]\d+)/);
if (match) osVersion = match[1].replace('_', '.');
}
}
else if (osInfo.userAgent.includes('iOS')) {
os = 'iOS';
const match = osInfo.userAgent.match(/OS (\d+[._]\d+)/);
if (match) osVersion = match[1].replace('_', '.');
}
// Enhanced browser detection
let browser = 'Unknown';
let browserVersion = '';
if (osInfo.userAgent.includes('Chrome')) {
browser = 'Chrome';
const match = osInfo.userAgent.match(/Chrome\/(\d+[._]\d+)/);
if (match) browserVersion = match[1].replace('_', '.');
}
else if (osInfo.userAgent.includes('Firefox')) {
browser = 'Firefox';
const match = osInfo.userAgent.match(/Firefox\/(\d+[._]\d+)/);
if (match) browserVersion = match[1].replace('_', '.');
}
else if (osInfo.userAgent.includes('Safari')) {
browser = 'Safari';
const match = osInfo.userAgent.match(/Version\/(\d+[._]\d+)/);
if (match) browserVersion = match[1].replace('_', '.');
}
else if (osInfo.userAgent.includes('Edge')) {
browser = 'Edge';
const match = osInfo.userAgent.match(/Edge\/(\d+[._]\d+)/);
if (match) browserVersion = match[1].replace('_', '.');
}
return [
<div key="neofetch" className="font-mono">
<div className="flex gap-8">
<div className="text-orange-500">
{` _____ _ _ _ `}
{`\n | __ \\ (_|_) | | `}
{`\n | |__) |___ _ __ _ __ _ _ ___ __| | _____ __`}
{`\n | _ // _ \\| '_ \\| '_ \\| | |/ _ \\ / _\` |/ _ \\ \\ / /`}
{`\n | | \\ \\ (_) | | | | | | | | | __/| (_| | __/\\ V / `}
{`\n |_| \\_\\___/|_| |_|_| |_|_|_|\\___(_)__,_|\\___| \\_/ `}
</div>
<div className="text-gray-400">
<div><span className="text-orange-500">OS:</span> {os}{osVersion ? ` ${osVersion}` : ''}</div>
<div><span className="text-orange-500">Host:</span> ronniie.dev</div>
<div><span className="text-orange-500">Kernel:</span> Next.js 15.0.3</div>
<div><span className="text-orange-500">Shell:</span> React Terminal</div>
<div><span className="text-orange-500">Terminal:</span> {browser}{browserVersion ? ` ${browserVersion}` : ''}</div>
<div><span className="text-orange-500">CPU:</span> {osInfo.cores} cores</div>
<div><span className="text-orange-500">Memory:</span> {osInfo.memory}</div>
<div><span className="text-orange-500">Uptime:</span> {context?.uptime || 'Unknown'}</div>
<div><span className="text-orange-500">Language:</span> {osInfo.language}</div>
</div>
</div>
</div>
];
},
};
export default neofetchCommand;

View file

@ -0,0 +1,98 @@
import { Command } from "../../types/command";
import projects from "../../data/projects.json";
const projectsCommand: Command = {
metadata: {
name: "projects",
description: "Show my projects",
icon: "🚀",
usage: "projects [project-name]",
},
execute: (args: string[] = []) => {
if (args.length === 0) {
// Show all projects
return [
<div key="projects" className="text-orange-500">
<div className="mb-4">Available Projects:</div>
<div className="grid grid-cols-1 gap-4">
{projects.projects.map((project) => (
<div key={project.name} className="border border-gray-700 rounded p-4">
<div className="flex items-center gap-2 mb-2">
<span className="text-orange-500">{project.icon}</span>
<span className="font-bold">{project.name}</span>
</div>
<p className="text-gray-400 mb-2">{project.description}</p>
<div className="flex flex-wrap gap-2">
{project.tags.map((tag) => (
<span key={tag} className="text-sm bg-gray-800 text-gray-300 px-2 py-1 rounded">
{tag}
</span>
))}
</div>
<a
href={project.url}
target="_blank"
rel="noopener noreferrer"
className="text-orange-500 hover:text-orange-400"
>
Visit Project
</a>
</div>
))}
</div>
</div>
];
}
// Show specific project
const projectName = args[0].toLowerCase();
const project = projects.projects.find(
(p) => p.name.toLowerCase() === projectName
);
if (!project) {
return [
<div key="project-error" className="text-red-400">
Error: Project not found
<div className="text-gray-400 mt-1">
Available projects: {projects.projects.map(p => p.name).join(", ")}
</div>
</div>
];
}
return [
<div key="project-detail" className="text-orange-500">
<div className="border border-gray-700 rounded p-6">
<div className="flex items-center gap-3 mb-4">
<span className="text-2xl">{project.icon}</span>
<h2 className="text-2xl font-bold">{project.name}</h2>
</div>
<p className="text-gray-300 mb-4">{project.description}</p>
<div className="mb-4">
<h3 className="text-yellow-400 mb-2">Technologies</h3>
<div className="flex flex-wrap gap-2">
{project.tags.map((tag) => (
<span key={tag} className="text-sm bg-gray-800 text-gray-300 px-2 py-1 rounded">
{tag}
</span>
))}
</div>
</div>
<div className="flex gap-4">
<a
href={project.url}
target="_blank"
rel="noopener noreferrer"
className="text-orange-500 hover:text-orange-400"
>
Visit Project
</a>
</div>
</div>
</div>
];
},
};
export default projectsCommand;

18
src/app/commands/pwd.tsx Normal file
View file

@ -0,0 +1,18 @@
import { Command } from "../../types/command";
const pwdCommand: Command = {
metadata: {
name: "pwd",
description: "Show current directory",
icon: "📁",
},
execute: () => {
return [
<div key="pwd" className="text-gray-400">
/home/ronnie
</div>
];
},
};
export default pwdCommand;

View file

@ -0,0 +1,31 @@
import { Command } from "../../types/command";
import dynamic from "next/dynamic";
const FaGithub = dynamic(() => import("react-icons/fa6").then((mod) => mod.FaGithub), { ssr: false });
const sourceCommand: Command = {
metadata: {
name: "source",
description: "View the source code of this terminal",
icon: "📄",
},
execute: () => [
<div key="source">
<strong className="text-orange-500">📄 Terminal Source Code:</strong>
<div className="text-gray-400 flex items-center mt-2">
<FaGithub className="text-gray-400 mr-2"/>
GitHub:{" "}
<a
href="https://github.com/Ronniie/ronniie.dev"
target="_blank"
rel="noopener noreferrer"
className="text-orange-500 hover:text-orange-400"
>
Ronniie/ronniie.dev
</a>
</div>
</div>,
],
};
export default sourceCommand;

View file

@ -0,0 +1,10 @@
const unknownCommand = (input: string) => () => [
<div key="unknown-command" className="text-red-500">
Unknown command: <span className="font-bold">{input}</span>
</div>,
<div key="suggestion" className="text-gray-400">
Type <span className="text-orange-500">help</span> to see available commands.
</div>,
];
export default unknownCommand;

View file

@ -0,0 +1,18 @@
import { Command } from "../../types/command";
const whoamiCommand: Command = {
metadata: {
name: "whoami",
description: "Show current user",
icon: "👤",
},
execute: () => {
return [
<div key="whoami" className="text-gray-400">
ronnie
</div>
];
},
};
export default whoamiCommand;

BIN
src/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

BIN
src/app/fonts/GeistVF.woff Normal file

Binary file not shown.

24
src/app/globals.css Normal file
View file

@ -0,0 +1,24 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--background: #ffffff;
--foreground: #171717;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
html,body {
max-width: 100vw;
overflow-x: hidden;
}
body {
color: var(--foreground);
background: var(--background);
font-family: 'Courier New', Courier, monospace;
}

74
src/app/layout.tsx Normal file
View file

@ -0,0 +1,74 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "ronniie.dev",
description: "A showcase of experimental projects hosted on *.ronniie.dev. Each project is a unique experiment, from fun visualizations to useful tools.",
keywords: "terminal, projects, web development, experiments, tools, utilities",
authors: [{ name: "Ronniie" }],
creator: "Ronniie",
publisher: "Ronniie",
robots: "index, follow",
icons: {
icon: [
{ url: '/favicon.png', sizes: 'any' },
{ url: '/favicon.png', sizes: '32x32', type: 'image/x-icon' },
{ url: '/favicon.png', sizes: '16x16', type: 'image/x-icon' },
],
apple: [
{ url: '/favicon.png', sizes: '180x180', type: 'image/x-icon' },
],
},
openGraph: {
type: 'website',
locale: 'en_US',
url: 'https://ronniie.dev',
title: "ronniie.dev",
description: "A showcase of experimental projects hosted on *.ronniie.dev. Each project is a unique experiment, from fun visualizations to useful tools.",
siteName: "ronniie.dev",
images: [
{
url: '/favicon.png',
width: 512,
height: 512,
alt: 'ronniie.dev favicon',
type: 'image/x-icon',
},
],
},
twitter: {
card: 'summary',
title: "ronniie.dev",
description: "A showcase of experimental projects hosted on *.ronniie.dev. Each project is a unique experiment, from fun visualizations to useful tools.",
creator: '@ronniie',
images: ['/favicon.png'],
},
manifest: '/manifest.json',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" className="!scroll-smooth" suppressHydrationWarning>
<head>
{/* Theme Colors */}
<meta name="theme-color" content="#f97316" />
<meta name="msapplication-navbutton-color" content="#f97316" />
<meta name="apple-mobile-web-app-status-bar-style" content="#f97316" />
{/* Favicon */}
<link rel="icon" type="image/png" sizes="32x32" href="/favicon.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon.png" />
<link rel="apple-touch-icon" href="/favicon.png" />
</head>
<body className={inter.className} suppressHydrationWarning>
{children}
</body>
</html>
);
}

9
src/app/page.tsx Normal file
View file

@ -0,0 +1,9 @@
import Terminal from "../components/Terminal";
export default function TerminalPage() {
return (
<div className="bg-black text-white w-full h-screen" suppressHydrationWarning >
<Terminal />
</div>
);
}

1
src/app/theme-color.ts Normal file
View file

@ -0,0 +1 @@
export default "#f97316";

4
src/app/viewport.ts Normal file
View file

@ -0,0 +1,4 @@
export default {
width: "device-width",
initialScale: 1,
};

298
src/components/Terminal.tsx Normal file
View file

@ -0,0 +1,298 @@
"use client";
import { useState, useRef, useEffect } from "react";
import { commands } from "../app/commands";
import unknownCommand from "../app/commands/unknown";
import projects from "../data/projects.json";
interface Project {
name: string;
description: string;
url: string;
icon: string;
tags: string[];
}
const Terminal: React.FC = () => {
const [mounted, setMounted] = useState(false);
const [input, setInput] = useState("");
const [output, setOutput] = useState<JSX.Element[]>([]);
const [history, setHistory] = useState<string[]>([]);
const [historyIndex, setHistoryIndex] = useState<number | null>(null);
const [suggestion, setSuggestion] = useState<string | null>(null);
const [inputDisabled, setInputDisabled] = useState(false);
const [startTime] = useState<Date>(new Date());
const terminalEndRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const commandsList = Object.keys(commands);
useEffect(() => {
setMounted(true);
setOutput([<MOTD key="motd" />]);
}, []);
const getUptime = () => {
const now = new Date();
const diff = now.getTime() - startTime.getTime();
const seconds = Math.floor(diff / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (days > 0) {
return `${days}d ${hours % 24}h ${minutes % 60}m`;
} else if (hours > 0) {
return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
} else if (minutes > 0) {
return `${minutes}m ${seconds % 60}s`;
} else {
return `${seconds}s`;
}
};
const handleInput = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (inputDisabled) return;
if (input.trim() === "") {
setOutput((prev) => [
...prev,
<div key={`blank-${prev.length}`}>&nbsp;</div>,
]);
setInput("");
return;
}
const normalizedInput = input.toLowerCase();
// Split by spaces but preserve quoted strings
const args = input.match(/(?:[^\s"]+|"[^"]*")+/g) || [];
if (args.length === 0) {
setInput("");
return;
}
const cmd = args[0]?.toLowerCase() || "";
const commandArgs = args.slice(1).map(arg => arg.replace(/^"|"$/g, '')); // Remove quotes
const command = commands[cmd];
if (normalizedInput === "clear") {
setOutput([]);
setInput("");
return;
}
if (command) {
if (normalizedInput === "exit") {
setInputDisabled(true);
}
setOutput((prev) => [
...prev,
<div key={`cmd-${input}`} className="text-gray-400">
<span className="text-orange-500">$</span> {input}
</div>,
...command.execute(commandArgs, { uptime: getUptime() }),
]);
} else {
setOutput((prev) => [
...prev,
<div key={`cmd-${input}`} className="text-gray-400">
<span className="text-orange-500">$</span> {input}
</div>,
...unknownCommand(normalizedInput)(),
]);
}
setHistory((prev) => [...prev, input]);
setHistoryIndex(null);
setInput("");
setSuggestion(null);
};
const handleInputChange = (value: string) => {
setInput(value);
const normalizedInput = value.toLowerCase();
const parts = normalizedInput.split(" ");
if (parts.length === 0) {
setSuggestion(null);
return;
}
const cmd = parts[0];
const args = parts.slice(1);
// Handle project name suggestions
if (cmd === "projects") {
const projectPrefix = args.join(" ").toLowerCase().replace(/^"|"$/g, '');
const matchedProject = projects.projects.find((project: Project) =>
project.name.toLowerCase().startsWith(projectPrefix)
);
if (matchedProject) {
setSuggestion(`projects "${matchedProject.name}"`);
} else {
setSuggestion(null);
}
return;
}
// Handle command suggestions
const matchedCommand = commandsList.find((command) =>
command.startsWith(normalizedInput)
);
setSuggestion(matchedCommand || null);
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "ArrowUp") {
e.preventDefault();
if (history.length === 0) return;
const newIndex = historyIndex === null
? history.length - 1
: Math.max(0, historyIndex - 1);
setHistoryIndex(newIndex);
setInput(history[newIndex]);
} else if (e.key === "ArrowDown") {
e.preventDefault();
if (historyIndex === null) return;
const newIndex = historyIndex + 1;
if (newIndex >= history.length) {
setHistoryIndex(null);
setInput("");
} else {
setHistoryIndex(newIndex);
setInput(history[newIndex]);
}
} else if (e.key === "Tab" && suggestion) {
e.preventDefault();
setInput(suggestion);
setSuggestion(null);
}
};
useEffect(() => {
terminalEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [output]);
if (!mounted) {
return null;
}
return (
<div
className="bg-black text-white font-mono w-full h-screen overflow-y-auto p-4"
onClick={() => inputRef.current?.focus()}
>
<div>
{output.map((line, index) => (
<div key={index} className="whitespace-pre-wrap">
{line}
</div>
))}
</div>
<form
onSubmit={handleInput}
className="flex items-center mt-2 relative"
>
{!inputDisabled && (
<span className="text-orange-500">$</span>
)}
{!inputDisabled && (
<div className="relative flex-1 ml-2">
<input
ref={inputRef}
type="text"
className="bg-black text-white outline-none w-full"
value={input}
onChange={(e) => handleInputChange(e.target.value)}
onKeyDown={handleKeyDown}
autoFocus
disabled={inputDisabled}
/>
{suggestion && (
<div
className="absolute top-0 left-0 text-gray-500"
style={{
opacity: 0.5,
pointerEvents: "none",
paddingLeft: "2px",
}}
>
{input}
<span className="text-gray-400">
{suggestion.slice(input.length)}
</span>
</div>
)}
</div>
)}
</form>
<div ref={terminalEndRef}></div>
</div>
);
};
const MOTD = () => {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return null;
}
return (
<>
<div
className="overflow-x-auto whitespace-pre font-mono text-xs sm:text-sm md:text-base text-orange-500"
>
{` _____ _ _ _ `}
{`\n | __ \\ (_|_) | | `}
{`\n | |__) |___ _ __ _ __ _ _ ___ __| | _____ __`}
{`\n | _ // _ \\| '_ \\| '_ \\| | |/ _ \\ / _\` |/ _ \\ \\ / /`}
{`\n | | \\ \\ (_) | | | | | | | | | __/| (_| | __/\\ V / `}
{`\n |_| \\_\\___/|_| |_|_| |_|_|_|\\___(_)__,_|\\___| \\_/ `}
</div>
<div className="text-orange-400 mt-4">
<strong>Welcome to Ronnie&#39;s Project Terminal!</strong>
</div>
<div className="border-t border-gray-700 my-4"/>
<strong className="text-orange-500">🚀 ABOUT THIS TERMINAL</strong>
<p className="text-gray-400">
This is a showcase of my experimental projects hosted on *.ronniie.dev.
</p>
<p className="text-gray-400">
Each project is a unique experiment, from fun visualizations to useful tools.
</p>
<p className="text-gray-400">
Feel free to explore and interact with them!
</p>
<div className="border-t border-gray-700 my-4"/>
<p className="text-orange-400">
🌟 &#34;Every project is an adventure in learning and creation.&#34; 🌟
</p>
<div className="border-t border-gray-700 my-4"/>
<div className="text-gray-400 mt-2">
Type <span className="text-orange-500">&#39;help&#39;</span> to see available commands. Let&#39;s explore
together!
</div>
<div className="text-gray-400 mt-2">
Want to learn more about me? Visit <a href="https://ronniie.com" target="_blank" rel="noopener noreferrer" className="text-orange-400 hover:text-orange-300">ronniie.com</a> for my full portfolio and blog.
</div>
<div className="text-gray-400 mt-1">
Available projects: {projects?.projects?.map(p => p.name).join(", ") || "None"}
</div>
</>
);
}
export default Terminal;

39
src/data/projects.json Normal file
View file

@ -0,0 +1,39 @@
{
"projects": [
{
"name": "Is My Internet Working?",
"description": "A simple tool to check if your internet connection is working. No fancy stuff, just a yes or no. Free, instant, and reliable internet connection checker.",
"icon": "🌐",
"url": "https://ismyinternetworking.ronniie.dev",
"tags": ["Web", "Fun", "Interactive", "JavaScript"]
},
{
"name": "What's My Vibe?",
"description": "Check your current vibe with our fun and interactive vibe checker. Get instant vibe readings and discover your mood!",
"icon": "😊",
"url": "https://whatsmyvibe.ronniie.dev",
"tags": ["Web", "Fun", "Interactive", "JavaScript"]
},
{
"name": "HoldThisButton",
"description": "Test your patience and endurance with our button-holding challenge. How long can you hold the button?",
"icon": "🔘",
"url": "https://holdthisbutton.ronniie.dev",
"tags": ["Web", "Fun", "Interactive", "JavaScript"]
},
{
"name": "TimelineCheck",
"description": "Check if you're in the right timeline with our timeline checker. Get instant timeline readings and discover if you're in the correct reality!",
"icon": "📰",
"url": "https://timelinecheck.ronniie.dev",
"tags": ["Web", "Fun", "Interactive", "JavaScript"]
},
{
"name": "ZoneOut",
"description": "A fast-paced square collection game where you select and clear bouncing squares. Choose your difficulty and game mode, then try to achieve the highest score! Game Modes: Levels (progress through increasingly challenging levels), Endless (play indefinitely with increasing difficulty). Difficulty Levels: Easy (infinite lives, slower squares, 20% red squares), Medium (5 lives, moderate speed, 30% red squares), Hard (3 lives, fast squares, 40% red squares). Features: Smooth selection, combo system, time bonuses, particle effects, modern UI, responsive design. Play at zoneout.ronniie.dev.",
"icon": "🟧",
"url": "https://zoneout.ronniie.dev",
"tags": ["Web", "Game", "Fun", "Interactive", "JavaScript", "Canvas"]
}
]
}

15
src/types/command.ts Normal file
View file

@ -0,0 +1,15 @@
export interface CommandMetadata {
name: string;
description: string;
icon: string;
usage?: string;
}
export interface CommandContext {
uptime?: string;
}
export interface Command {
metadata: CommandMetadata;
execute: (args?: string[], context?: CommandContext) => JSX.Element[];
}