Compare commits

...

2 Commits

6 changed files with 218 additions and 89 deletions

View File

@ -3,7 +3,7 @@
pkgs.mkShell {
buildInputs = with pkgs; [
# Node.js and package managers
nodejs_20
nodejs_23
nodePackages.npm
nodePackages.yarn

View File

@ -149,8 +149,8 @@ function App() {
{/* Your gothic header */}
<div className="fixed top-0 left-0 w-full text-center pt-8 z-10 gothic-text">
<h1 className="text-6xl text-purple-300 mb-2">Npc-About-Me</h1>
<h2 className="text-xl text-purple-400">Console View</h2>
<h1 className="text-6xl text-purple-300 mb-2">Npc About Me</h1>
<h2 className="text-3xl text-purple-400">Console View</h2>
</div>
{/* Your terminal component */}
@ -179,13 +179,13 @@ function App() {
<div className="h-3 w-3 bg-yellow-500 rounded-full hover:bg-yellow-600 transition-colors"></div>
<div className="h-3 w-3 bg-green-500 rounded-full hover:bg-green-600 transition-colors"></div>
</div>
<span className="mx-auto text-sm text-gray-300 font-medium">
<span className="mx-auto text-gray-300 gothic-text text-xl">
terminal@npc-about-me
</span>
</div>
{/* Terminal component with better background */}
<div className="bg-terminal-bg w-full h-[calc(100%-2.5rem)] overflow-hidden bg-[#0d1117]">
<div className="bg-terminal-bg w-full h-[calc(100%-2.5rem)] overflow-hidden">
<Terminal />
</div>

View File

@ -17,22 +17,67 @@ const BackgroundImages = () => {
"/bg-ims/blahaj.png",
];
// Function to generate random values within a range
const randomRange = (min, max) => Math.random() * (max - min) + min;
// Function to generate new position and animation values
const generateImageData = () => {
// Generate random movement offsets
const moveX1 = randomRange(-20, 20);
const moveY1 = randomRange(-20, 20);
const moveX2 = randomRange(-20, 20);
const moveY2 = randomRange(-20, 20);
const moveX3 = randomRange(-20, 20);
const moveY3 = randomRange(-20, 20);
const rotateDeg = randomRange(-20, 20);
return {
src: bgImages[Math.floor(Math.random() * bgImages.length)],
size: randomRange(50, 150),
opacity: randomRange(0.1, 0.3),
// Animation durations (in seconds)
moveDuration: randomRange(60, 120),
rotateDuration: randomRange(30, 60),
// Starting position
startTop: randomRange(5, 90),
startLeft: randomRange(5, 90),
// Pre-calculated movement values
moveX1,
moveY1,
moveX2,
moveY2,
moveX3,
moveY3,
rotateDeg,
};
};
useEffect(() => {
// Generate 30 random positioned images
const randomImages = [];
for (let i = 0; i < 30; i++) {
randomImages.push({
src: bgImages[Math.floor(Math.random() * bgImages.length)],
top: Math.random() * 90 + 5, // 5-95% from top
left: Math.random() * 90 + 5, // 5-95% from left
size: Math.random() * 100 + 50, // 50-150px
rotation: Math.random() * 40 - 20, // -20 to 20 degrees
opacity: Math.random() * 0.2 + 0.1, // 0.1-0.3 opacity
});
}
setImages(randomImages);
// Generate initial images
const initialImages = Array(30).fill(null).map(generateImageData);
setImages(initialImages);
}, []);
// Helper to create the keyframes CSS
const generateKeyframesStyles = () => {
return images
.map(
(img, index) => `
@keyframes float-${index} {
0%, 100% { transform: translate(-50%, -50%) translate(0, 0); }
25% { transform: translate(-50%, -50%) translate(${img.moveX1}px, ${img.moveY1}px); }
50% { transform: translate(-50%, -50%) translate(${img.moveX2}px, ${img.moveY2}px); }
75% { transform: translate(-50%, -50%) translate(${img.moveX3}px, ${img.moveY3}px); }
}
@keyframes rotate-${index} {
0%, 100% { transform: translate(-50%, -50%) rotate(0deg); }
50% { transform: translate(-50%, -50%) rotate(${img.rotateDeg}deg); }
}
`
)
.join("\n");
};
return (
<div className="fixed inset-0 overflow-hidden pointer-events-none z-0">
{images.map((img, index) => (
@ -40,9 +85,13 @@ const BackgroundImages = () => {
key={index}
className="absolute"
style={{
top: `${img.top}%`,
left: `${img.left}%`,
transform: `translate(-50%, -50%) rotate(${img.rotation}deg)`,
top: `${img.startTop}%`,
left: `${img.startLeft}%`,
transform: "translate(-50%, -50%)",
animation: `
float-${index} ${img.moveDuration}s infinite ease-in-out,
rotate-${index} ${img.rotateDuration}s infinite ease-in-out
`,
}}
>
<img
@ -57,6 +106,8 @@ const BackgroundImages = () => {
/>
</div>
))}
<style dangerouslySetInnerHTML={{ __html: generateKeyframesStyles() }} />
</div>
);
};

View File

@ -8,6 +8,7 @@ export default function Terminal() {
const inputRef = useRef(null);
const terminalRef = useRef(null);
const [hasBeenCleared, setHasBeenCleared] = useState(false);
const [historyIndex, setHistoryIndex] = useState(-1);
// Focus the input when the component mounts and on click
useEffect(() => {
@ -42,6 +43,12 @@ export default function Terminal() {
handleClear();
setInputValue("");
return;
} else if (e.key === "ArrowDown") {
e.preventDefault();
navigateHistory(-1);
} else if (e.key === "ArrowUp") {
e.preventDefault();
navigateHistory(1);
}
};
@ -134,6 +141,7 @@ export default function Terminal() {
"help",
"clear",
"view",
"tree",
];
const matchingCommands = commands.filter((cmd) =>
cmd.startsWith(command)
@ -146,7 +154,7 @@ export default function Terminal() {
}
// If completing a path/file/directory
if (["cd", "cat", "ls", "view"].includes(command)) {
if (["cd", "cat", "ls", "view", "tree"].includes(command)) {
const partialPath = parts[parts.length - 1];
const suggestions = getPathSuggestions(partialPath);
@ -188,6 +196,7 @@ export default function Terminal() {
const executeCommand = () => {
if (!inputValue.trim()) return;
setHistoryIndex(-1);
// Create a new history entry with the CURRENT PATH at execution time
const newHistoryEntry = {
command: inputValue,
@ -229,6 +238,7 @@ export default function Terminal() {
" view [image] - Display image files\n" +
" pwd - Print working directory\n" +
" clear - Clear the terminal\n" +
" tree [directory] - Display directory structure\n" +
" help - Display this help message\n\n" +
"Shortcuts:\n" +
" Tab - Autocomplete commands and paths\n" +
@ -238,6 +248,9 @@ export default function Terminal() {
clearTerminal();
setInputValue("");
return;
case "tree":
output = handleTree(args);
break;
default:
output = `Command not found: ${command}`;
}
@ -260,23 +273,48 @@ export default function Terminal() {
// Handle ls command
const handleLs = (args) => {
let targetPath = currentPath;
let showHidden = false;
let pathArgs = [];
if (args.length > 0) {
// Handle path argument
targetPath = resolvePath(args[0]);
// Parse arguments
args.forEach((arg) => {
if (arg.startsWith("-")) {
// Handle flags
if (arg === "-a" || arg === "--all") {
showHidden = true;
} else if (arg === "--help") {
return `Usage: ls [OPTION]... [FILE]...
List information about the FILEs (the current directory by default).
Sort entries alphabetically.
-a, --all do not ignore entries starting with .
--help display this help and exit`;
}
} else {
// It's a path argument
pathArgs.push(arg);
}
});
// Handle path argument if provided
if (pathArgs.length > 0) {
targetPath = resolvePath(pathArgs[0]);
}
const item = getItemAtPath(targetPath);
if (!item) {
return `ls: ${args[0]}: No such file or directory`;
return `ls: ${pathArgs[0]}: No such file or directory`;
}
if (item.type !== "directory") {
return `ls: ${args[0]}: Not a directory`;
return `ls: ${pathArgs[0]}: Not a directory`;
}
const children = Object.keys(item.children).sort();
// Get and filter children based on showHidden flag
const children = Object.keys(item.children)
.filter((name) => showHidden || !name.startsWith("."))
.sort();
if (children.length === 0) {
return "<span class='text-gray-400'>Empty directory</span>";
@ -345,20 +383,36 @@ export default function Terminal() {
}
const fileName = args[0];
const currentDir = getItemAtPath(currentPath);
let targetPath = fileName;
if (!currentDir || currentDir.type !== "directory") {
return "Current location is not a directory";
// If it's not an absolute path, resolve it relative to current directory
if (!fileName.startsWith("/")) {
targetPath = resolvePath(fileName);
}
if (
fileName in currentDir.children &&
currentDir.children[fileName].type === "file"
) {
return currentDir.children[fileName].content;
// Get the directory and file name from the path
const lastSlashIndex = targetPath.lastIndexOf("/");
const dirPath =
lastSlashIndex === -1
? currentPath
: targetPath.substring(0, lastSlashIndex);
const targetFileName =
lastSlashIndex === -1
? targetPath
: targetPath.substring(lastSlashIndex + 1);
const dir = getItemAtPath(dirPath);
if (!dir || dir.type !== "directory") {
return `cat: ${fileName}: No such file or directory`;
}
return `cat: ${fileName}: No such file`;
const file = dir.children[targetFileName];
if (!file || file.type !== "file") {
return `cat: ${fileName}: No such file`;
}
return file.content;
};
// Handle view command for images
@ -407,6 +461,60 @@ export default function Terminal() {
</div>`;
};
// Add tree command handler
const handleTree = (args) => {
const path = args[0] || currentPath;
const resolvedPath = resolvePath(path);
const item = getItemAtPath(resolvedPath);
if (!item) {
return `tree: ${path}: No such directory`;
}
if (item.type !== "directory") {
return `tree: ${path}: Not a directory`;
}
const output = [];
const printTree = (node, name, prefix = "", isLast = true) => {
const isDir = node.type === "directory";
const icon = isDir ? "📁" : "📄";
output.push(`${prefix}${isLast ? "└── " : "├── "}${icon} ${name}`);
if (isDir && node.children) {
const entries = Object.entries(node.children);
entries.forEach(([key, value], index) => {
const isLastEntry = index === entries.length - 1;
const newPrefix = prefix + (isLast ? " " : "│ ");
printTree(value, key, newPrefix, isLastEntry);
});
}
};
// Start with the root directory name
const rootName = resolvedPath.split("/").pop() || "/";
printTree(item, rootName);
return output.join("\n");
};
// Function to navigate command history
const navigateHistory = (direction) => {
if (commandHistory.length === 0) return;
const newIndex = historyIndex + direction;
if (newIndex >= -1 && newIndex < commandHistory.length) {
setHistoryIndex(newIndex);
if (newIndex === -1) {
setInputValue("");
} else {
setInputValue(
commandHistory[commandHistory.length - 1 - newIndex].command
);
}
}
};
return (
<div
className="h-full text-terminal-text font-mono p-4 overflow-hidden flex flex-col text-left bg-gradient-to-b from-gray-900 to-black rounded-b-lg"
@ -425,20 +533,13 @@ export default function Terminal() {
>
{!hasBeenCleared && (
<div className="text-lg mb-4 text-terminal-accent text-left">
<pre className="text-purple-300 font-bold">
{`
_ _ ____ ____ _ ____ ___ _ _ _____ __ __ _____
| \\ | | _ \\ / ___| / \\ | __ ) / _ \\| | | |_ _| | \\/ | ____|
| \\| | |_) | | / _ \\ | _ \\| | | | | | | | | | |\\/| | _|
| |\\ | __/| |___ / ___ \\| |_) | |_| | |_| | | | | | | | |___
|_| \\_|_| \\____| /_/ \\_\\____/ \\___/ \\___/ |_| |_| |_|_____|
`}
</pre>
<div className="text-green-400 font-semibold mb-2">
<div className="text-purple-300 gothic-text text-4xl tracking-wider">
NPC ABOUT ME
</div>
<div className="text-green-400 gothic-text font-semibold mb-2 text-2xl">
Welcome to my interactive terminal!
</div>
<div className="text-amber-300 mb-3">
<div className="text-amber-300 gothic-text mb-3 text-xl">
Type{" "}
<span className="bg-gray-800 px-1 rounded text-white">help</span>{" "}
to see available commands.
@ -449,7 +550,7 @@ export default function Terminal() {
{commandHistory.map((entry, index) => (
<div key={index} className="mb-2 text-left">
<div className="flex">
<span className="text-terminal-prompt mr-2">
<span className="text-terminal-prompt mr-2 gothic-text text-xl">
guest@npc-shell:
{entry.path === "/home/guest"
? "~"
@ -479,7 +580,7 @@ export default function Terminal() {
className="w-full"
>
<div className="flex items-center">
<span className="text-terminal-prompt mr-2 flex items-center">
<span className="text-terminal-prompt mr-2 flex items-center gothic-text text-xl">
<span className="text-green-400">guest</span>
<span className="text-gray-500">@</span>
<span className="text-purple-400">npc-shell</span>

View File

@ -17,7 +17,12 @@ export const fileSystem = {
"about.txt": {
type: "file",
content:
"Hi there! I'm a developer passionate about creating interactive web experiences.\n\nI enjoy working with modern JavaScript frameworks, building intuitive UIs, and exploring new technologies.\n\nWhen I'm not coding, you can find me gaming, reading sci-fi, or hanging out with my Blahaj.",
"I'm the average Internet person. I currently code and play video games :) I especially play Euro Truck Sim 2 and Roblox, Phantom Forces to be exact. Sometimes I play Minecraft with my friends :D.\n\nI realy love Ikea's stuffed animals\n\nI can Program in html, css, python, c++, c#, c and maybe i can do minecraft mods too :]",
},
"status.txt": {
type: "file",
content:
"Status :)\nHi there! Here are some details about me:\n\nAge: 14\nPronouns: He/Him ^_^\nRelationship Status: Single :D\nFun Fact 1: I'm an NPC! :O Just doing my thing, not needing to be in the center of attention or talk much. :|\nFun Fact 2: I really like the Ikea Plushies. (BLÅHAJ on top)",
},
".secret": {
type: "file",
@ -32,30 +37,9 @@ export const fileSystem = {
content:
"Terminal Portfolio\n------------------\n\nAn interactive terminal-style portfolio website built with React and Tailwind CSS.\n\nFeatures:\n- Fully functional command line interface\n- Virtual filesystem navigation\n- Custom commands\n- Responsive design",
},
"project1.txt": {
type: "file",
content:
"Project 1\n---------\n\nA full-stack web application for task management.\n\nTechnologies:\n- React\n- Node.js\n- MongoDB\n- Express",
},
"project2.txt": {
type: "file",
content:
"Project 2\n---------\n\nAn e-commerce platform with real-time inventory tracking.\n\nTechnologies:\n- Vue.js\n- Firebase\n- Stripe API\n- Tailwind CSS",
},
screenshots: {
type: "directory",
children: {
"project1.png": {
type: "image",
src: "/images/project1.png",
description: "Screenshot of Project 1",
},
"project2.png": {
type: "image",
src: "/images/project2.png",
description: "Screenshot of Project 2",
},
},
children: {},
},
},
},
@ -65,17 +49,17 @@ export const fileSystem = {
"frontend.txt": {
type: "file",
content:
"Frontend Skills\n--------------\n\n- HTML5, CSS3, JavaScript\n- React, Vue.js\n- Tailwind CSS, SASS\n- TypeScript\n- Responsive Design\n- Webpack, Vite",
"Frontend Skills\n--------------\n\n- HTML5, CSS, JavaScript, TypeScript\n- React, Vue.js\n- Tailwind CSS, Bootstrap\n- Responsive Design\n- Vite",
},
"backend.txt": {
type: "file",
content:
"Backend Skills\n-------------\n\n- Node.js, Express\n- Python, Django\n- RESTful APIs\n- GraphQL\n- MongoDB, PostgreSQL\n- Firebase, AWS",
"Backend Skills\n-------------\n\n- Node.js\n- Python\n- Flask APIs\n- ChartJS\n- MariaDB, PostgreSQL\n- Docker, Proxmox",
},
"tools.txt": {
type: "file",
content:
"Development Tools\n----------------\n\n- Git, GitHub\n- Docker\n- VS Code\n- Figma\n- Jest, Cypress\n- CI/CD Pipelines",
"Development Tools\n----------------\n\n- Gitea, GitHub\n- Docker\n- VS Code/Cursor\n- Proxmox, Linux",
},
},
},
@ -84,15 +68,15 @@ export const fileSystem = {
children: {
"email.txt": {
type: "file",
content: "Email: example@domain.com",
content: "Email: npc.lele@gmail.com",
},
"github.txt": {
type: "file",
content: "GitHub: https://github.com/yourusername",
content: "GitHub: https://github.com/Lele7663",
},
"linkedin.txt": {
"discord.txt": {
type: "file",
content: "LinkedIn: https://linkedin.com/in/yourprofile",
content: "jxstnpc",
},
},
},
@ -147,7 +131,7 @@ export const fileSystem = {
"coding-music.txt": {
type: "file",
content:
"Best Music for Coding\n------------------\n\n- Lo-fi beats\n- Ambient electronic\n- Video game soundtracks\n- Classical music\n- Synthwave\n\nPersonally, I find that music without lyrics helps me focus best when tackling complex problems.",
"Best Music for Coding\n------------------\n- Anything that sounds good!\n(based answer, i know)",
},
},
},

View File

@ -132,13 +132,6 @@ button:focus-visible {
}
/* Add these styles to your index.css */
@font-face {
font-family: "CloisterBlack";
src: url("/CloisterBlack.ttf") format("truetype");
font-weight: normal;
font-style: normal;
}
.terminal-window {
transition: box-shadow 0.3s ease;
}