Initial commit (history reset)
This commit is contained in:
commit
173a4fc272
47 changed files with 7016 additions and 0 deletions
45
src/app/commands/MOTD.tsx
Normal file
45
src/app/commands/MOTD.tsx
Normal 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'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">
|
||||
🌟 "Every project is an adventure in learning and creation." 🌟
|
||||
</p>
|
||||
|
||||
<div className="border-t border-gray-700 my-4"/>
|
||||
<div className="text-gray-400 mt-2">
|
||||
Type <span className="text-orange-500">'help'</span> to see available commands. Let'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;
|
15
src/app/commands/clear.tsx
Normal file
15
src/app/commands/clear.tsx
Normal 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
28
src/app/commands/date.tsx
Normal 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
29
src/app/commands/echo.tsx
Normal 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 <text>
|
||||
</div>
|
||||
</div>
|
||||
];
|
||||
}
|
||||
return [
|
||||
<div key="echo" className="text-gray-400">
|
||||
{args.join(" ")}
|
||||
</div>
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default echoCommand;
|
20
src/app/commands/exit.tsx
Normal file
20
src/app/commands/exit.tsx
Normal 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
104
src/app/commands/help.tsx
Normal 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 <text></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
26
src/app/commands/index.ts
Normal 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
26
src/app/commands/ls.tsx
Normal 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;
|
133
src/app/commands/neofetch.tsx
Normal file
133
src/app/commands/neofetch.tsx
Normal 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;
|
98
src/app/commands/projects.tsx
Normal file
98
src/app/commands/projects.tsx
Normal 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
18
src/app/commands/pwd.tsx
Normal 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;
|
31
src/app/commands/source.tsx
Normal file
31
src/app/commands/source.tsx
Normal 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;
|
10
src/app/commands/unknown.tsx
Normal file
10
src/app/commands/unknown.tsx
Normal 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;
|
18
src/app/commands/whoami.tsx
Normal file
18
src/app/commands/whoami.tsx
Normal 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
BIN
src/app/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
BIN
src/app/fonts/GeistMonoVF.woff
Normal file
BIN
src/app/fonts/GeistMonoVF.woff
Normal file
Binary file not shown.
BIN
src/app/fonts/GeistVF.woff
Normal file
BIN
src/app/fonts/GeistVF.woff
Normal file
Binary file not shown.
24
src/app/globals.css
Normal file
24
src/app/globals.css
Normal 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
74
src/app/layout.tsx
Normal 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
9
src/app/page.tsx
Normal 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
1
src/app/theme-color.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export default "#f97316";
|
4
src/app/viewport.ts
Normal file
4
src/app/viewport.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export default {
|
||||
width: "device-width",
|
||||
initialScale: 1,
|
||||
};
|
298
src/components/Terminal.tsx
Normal file
298
src/components/Terminal.tsx
Normal 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}`}> </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'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">
|
||||
🌟 "Every project is an adventure in learning and creation." 🌟
|
||||
</p>
|
||||
|
||||
<div className="border-t border-gray-700 my-4"/>
|
||||
<div className="text-gray-400 mt-2">
|
||||
Type <span className="text-orange-500">'help'</span> to see available commands. Let'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
39
src/data/projects.json
Normal 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
15
src/types/command.ts
Normal 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[];
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue