diff --git a/src/components/NavigationBar.tsx b/src/components/NavigationBar.tsx
index 08bf934..42e2136 100644
--- a/src/components/NavigationBar.tsx
+++ b/src/components/NavigationBar.tsx
@@ -3,11 +3,12 @@ import { useEffect } from "preact/hooks";
import {
Home,
NotebookPen,
- FileText,
+ BriefcaseBusiness,
CodeXml,
Terminal as TerminalIcon,
Megaphone,
} from "lucide-preact";
+import { navigationItems } from '../config/data';
interface NavigationBarProps {
currentPath: string;
@@ -66,8 +67,13 @@ export default function NavigationBar({ currentPath }: NavigationBarProps) {
? activePath.slice(0, -1)
: activePath;
- const isPostsPath = (path: string) => {
- return path.startsWith("/posts") || path.startsWith("/post/");
+ const iconMap = {
+ Home,
+ NotebookPen,
+ BriefcaseBusiness,
+ CodeXml,
+ TerminalIcon,
+ Megaphone,
};
useEffect(() => {
@@ -100,70 +106,22 @@ export default function NavigationBar({ currentPath }: NavigationBarProps) {
>
- -
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
+ {navigationItems.map((item) => {
+ const Icon = iconMap[item.icon as keyof typeof iconMap];
+ const isActive = item.isActive
+ ? item.isActive(normalizedPath)
+ : normalizedPath === item.path;
+
+ return (
+ -
+
+
+
+
+
+
+ );
+ })}
diff --git a/src/components/ProjectCard.astro b/src/components/ProjectCard.astro
index 31b4eec..2efe8d0 100644
--- a/src/components/ProjectCard.astro
+++ b/src/components/ProjectCard.astro
@@ -1,12 +1,6 @@
---
import { Icon } from 'astro-icon/components';
-
-interface Project {
- id: string;
- name: string;
- description: string;
- link: string;
-}
+import type { Project } from '../config/data';
export interface Props {
project: Project;
diff --git a/src/components/SocialLinks.astro b/src/components/SocialLinks.astro
index cc2137f..8620d76 100644
--- a/src/components/SocialLinks.astro
+++ b/src/components/SocialLinks.astro
@@ -1,44 +1,23 @@
---
import { Icon } from 'astro-icon/components';
import SpotifyIcon from './SpotifyIcon';
+import { socialLinks } from '../config/data';
---
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ {socialLinks.map((link) => (
+ link.id === 'spotify' ? (
+
+ ) : (
+
+
+
+ )
+ ))}
+
diff --git a/src/components/TalkCard.astro b/src/components/TalkCard.astro
index bb08d7b..ccccab7 100644
--- a/src/components/TalkCard.astro
+++ b/src/components/TalkCard.astro
@@ -1,12 +1,6 @@
---
import { Icon } from "astro-icon/components";
-
-interface Talk {
- id: string;
- name: string;
- description: string;
- link: string;
-}
+import type { Talk } from '../config/data';
export interface Props {
talk: Talk;
diff --git a/src/components/TechLinks.astro b/src/components/TechLinks.astro
index 7142530..ac20bfb 100644
--- a/src/components/TechLinks.astro
+++ b/src/components/TechLinks.astro
@@ -1,74 +1,17 @@
---
import { Icon } from 'astro-icon/components';
+import { techLinks } from '../config/data';
---
+ {techLinks.map((link) => (
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
+ ))}
+
\ No newline at end of file
diff --git a/src/components/Terminal.tsx b/src/components/Terminal.tsx
index 9964c99..25d7f63 100644
--- a/src/components/Terminal.tsx
+++ b/src/components/Terminal.tsx
@@ -1,49 +1,29 @@
-import { useState, useEffect, useRef } from 'preact/hooks';
-import type { JSX } from 'preact';
-
-interface FileSystemNode {
- type: 'directory' | 'file';
- name: string;
- content?: string;
- children?: { [key: string]: FileSystemNode };
-}
-
-interface ResumeData {
- basics: {
- name: string;
- email: string;
- url?: { href: string };
- };
- sections: {
- summary: { name: string; content: string };
- profiles: { name: string; items: { network: string; username: string; url: { href: string } }[] };
- skills: { name: string; items: { id: string; name: string; level: number }[] };
- experience: { name: string; items: { id: string; company: string; position: string; date: string; location: string; summary: string; url?: { href: string } }[] };
- education: { name: string; items: { id: string; institution: string; studyType: string; area: string; date: string; summary: string }[] };
- volunteer: { name: string; items: { id: string; organization: string; position: string; date: string }[] };
- };
-}
-
-interface Command {
- input: string;
- output: string;
- timestamp: Date;
- path: string;
-}
+import { useState, useEffect, useRef } from "preact/hooks";
+import type { JSX } from "preact";
+import type { Command } from "../lib/terminal/types";
+import { buildFileSystem } from "../lib/terminal/fileSystem";
+import { executeCommand, type CommandContext } from "../lib/terminal/commands";
+import {
+ getCompletions,
+ formatOutput,
+ saveCommandToHistory,
+ loadCommandHistory,
+} from "../lib/terminal/utils";
const Terminal = () => {
- const [currentPath, setCurrentPath] = useState('/');
+ const [currentPath, setCurrentPath] = useState("/");
const [commandHistory, setCommandHistory] = useState([
{
- input: '',
- output: 'Welcome to Atridad\'s Shell!\nType "help" to see available commands.\n',
+ input: "",
+ output:
+ 'Welcome to Atridad\'s Shell!\nType "help" to see available commands.\n',
timestamp: new Date(),
- path: '/'
- }
+ path: "/",
+ },
]);
- const [currentInput, setCurrentInput] = useState('');
+ const [currentInput, setCurrentInput] = useState("");
const [historyIndex, setHistoryIndex] = useState(-1);
- const [fileSystem, setFileSystem] = useState<{ [key: string]: FileSystemNode }>({});
+ const [fileSystem, setFileSystem] = useState<{ [key: string]: any }>({});
const [isTrainRunning, setIsTrainRunning] = useState(false);
const [trainPosition, setTrainPosition] = useState(100);
const [persistentHistory, setPersistentHistory] = useState([]);
@@ -62,540 +42,91 @@ const Terminal = () => {
}
}, []);
- // Load persistent command history from localStorage
+ // Load command history from localStorage
useEffect(() => {
- const savedHistory = localStorage.getItem('terminal-history');
- if (savedHistory) {
- try {
- const parsed = JSON.parse(savedHistory);
- setPersistentHistory(parsed);
- } catch (error) {
- console.error('Error loading command history:', error);
- }
- }
+ const history = loadCommandHistory();
+ setPersistentHistory(history);
}, []);
- // Save command history to localStorage
- const saveCommandToHistory = (command: string) => {
- if (command.trim()) {
- const updatedHistory = [...persistentHistory, command].slice(-100); // Keep last 100 commands
- setPersistentHistory(updatedHistory);
- localStorage.setItem('terminal-history', JSON.stringify(updatedHistory));
- }
- };
-
+ // Initialize file system
useEffect(() => {
- const initializeFileSystem = async () => {
- try {
- const response = await fetch('/files/resume.json');
- const resumeData: ResumeData = await response.json();
-
- // Fetch blog posts
- const postsResponse = await fetch('/api/posts.json');
- let postsData = [];
- try {
- postsData = await postsResponse.json();
- } catch (error) {
- console.log('Could not fetch posts data:', error);
- }
-
- // Build resume files from actual data
- const resumeFiles: { [key: string]: FileSystemNode } = {};
-
- if (resumeData.sections.summary) {
- resumeFiles['summary.txt'] = {
- type: 'file',
- name: 'summary.txt',
- content: resumeData.sections.summary.content.replace(/<[^>]*>/g, '')
- };
- }
-
- if (resumeData.sections.skills?.items) {
- const skillsContent = resumeData.sections.skills.items
- .map(skill => `${skill.name} (Level: ${skill.level}/5)`)
- .join('\n');
- resumeFiles['skills.txt'] = {
- type: 'file',
- name: 'skills.txt',
- content: skillsContent
- };
- }
-
- if (resumeData.sections.experience?.items) {
- const experienceContent = resumeData.sections.experience.items
- .map(exp => {
- const summary = exp.summary.replace(/<[^>]*>/g, '').replace(/ /g, ' ');
- return `${exp.position} at ${exp.company}\n${exp.date} | ${exp.location}\n${summary}\n${exp.url?.href ? `URL: ${exp.url.href}` : ''}\n`;
- })
- .join('\n---\n\n');
- resumeFiles['experience.txt'] = {
- type: 'file',
- name: 'experience.txt',
- content: experienceContent
- };
- }
-
- if (resumeData.sections.education?.items) {
- const educationContent = resumeData.sections.education.items
- .map(edu => `${edu.institution}\n${edu.studyType} - ${edu.area}\n${edu.date}\n${edu.summary ? edu.summary.replace(/<[^>]*>/g, '') : ''}`)
- .join('\n\n---\n\n');
- resumeFiles['education.txt'] = {
- type: 'file',
- name: 'education.txt',
- content: educationContent
- };
- }
-
- if (resumeData.sections.volunteer?.items) {
- const volunteerContent = resumeData.sections.volunteer.items
- .map(vol => `${vol.organization}\n${vol.position}\n${vol.date}`)
- .join('\n\n---\n\n');
- resumeFiles['volunteer.txt'] = {
- type: 'file',
- name: 'volunteer.txt',
- content: volunteerContent
- };
- }
-
- // Build posts files from actual blog posts
- const postsFiles: { [key: string]: FileSystemNode } = {
- 'README.txt': {
- type: 'file',
- name: 'README.txt',
- content: 'Blog posts and articles.\n\nUse "open /posts" to see the full list on the website.\n\nAvailable posts:\n' +
- postsData.map((post: any) => `- ${post.slug}.md`).join('\n')
- }
- };
-
- // Add each blog post as a markdown file
- postsData.forEach((post: any) => {
- const fileName = `${post.slug}.md`;
- let content = `---
-title: "${post.title}"
-description: "${post.description}"
-pubDate: "${post.pubDate}"
-tags: [${post.tags.map((tag: string) => `"${tag}"`).join(', ')}]
----
-
-${post.content}`;
-
- postsFiles[fileName] = {
- type: 'file',
- name: fileName,
- content
- };
- });
-
- // Build contact info from resume profiles
- const contactContent = [
- `Email: ${resumeData.basics.email}`,
- '',
- 'Social Profiles:',
- ...resumeData.sections.profiles.items.map(profile =>
- `${profile.network}: ${profile.url.href}`
- )
- ].join('\n');
-
- const fs: { [key: string]: FileSystemNode } = {
- '/': {
- type: 'directory',
- name: '/',
- children: {
- 'about.txt': {
- type: 'file',
- name: 'about.txt',
- content: `${resumeData.basics.name}\nResearcher, Full-Stack Developer, and IT Professional.\n\nVisit other sections to learn more about my work and experience.`
- },
- 'resume': {
- type: 'directory',
- name: 'resume',
- children: resumeFiles
- },
- 'posts': {
- type: 'directory',
- name: 'posts',
- children: postsFiles
- },
- 'projects': {
- type: 'directory',
- name: 'projects',
- children: {
- 'README.txt': {
- type: 'file',
- name: 'README.txt',
- content: 'Personal and professional projects.\n\nUse "open /projects" to see the full portfolio on the website.'
- }
- }
- },
- 'contact.txt': {
- type: 'file',
- name: 'contact.txt',
- content: contactContent
- }
- }
- }
- };
-
- setFileSystem(fs);
- } catch (error) {
- console.error('Error loading resume data:', error);
- // Fallback to basic file system
- setFileSystem({
- '/': {
- type: 'directory',
- name: '/',
- children: {
- 'about.txt': {
- type: 'file',
- name: 'about.txt',
- content: 'Atridad Lahiji\nResearcher, Full-Stack Developer, and IT Professional.\n\nError loading detailed information. Please check the website directly.'
- }
- }
- }
- });
- }
- };
-
- initializeFileSystem();
+ buildFileSystem().then(setFileSystem);
}, []);
- const getCurrentDirectory = (): FileSystemNode => {
- const pathParts = currentPath.split('/').filter((part: string) => part !== '');
- let current = fileSystem['/'];
-
- for (const part of pathParts) {
- if (current?.children && current.children[part] && current.children[part].type === 'directory') {
- current = current.children[part];
- }
- }
-
- return current;
- };
-
- const resolvePath = (path: string): string => {
- if (path.startsWith('/')) {
- return path;
- }
-
- const currentParts = currentPath.split('/').filter((part: string) => part !== '');
- const pathParts = path.split('/');
-
- for (const part of pathParts) {
- if (part === '..') {
- currentParts.pop();
- } else if (part !== '.' && part !== '') {
- currentParts.push(part);
- }
- }
-
- return '/' + currentParts.join('/');
- };
-
- const getCompletions = (input: string): { completion: string | null, replaceFrom: number } => {
- const parts = input.trim().split(' ');
- const command = parts[0];
- const partialPath = parts[parts.length - 1] || '';
-
- // Only complete paths for these commands, not the commands themselves
- if (parts.length > 1 && ['ls', 'cd', 'cat', 'open'].includes(command)) {
- // Path completion
- const isAbsolute = partialPath.startsWith('/');
- const pathToComplete = isAbsolute ? partialPath : resolvePath(partialPath);
-
- // Find the directory to search in and the prefix to match
- let dirPath: string;
- let searchPrefix: string;
-
- if (pathToComplete.endsWith('/')) {
- // Path ends with slash - complete inside this directory
- dirPath = pathToComplete;
- searchPrefix = '';
- } else {
- // Base case - find directory and prefix
- const lastSlash = pathToComplete.lastIndexOf('/');
- if (lastSlash >= 0) {
- dirPath = pathToComplete.substring(0, lastSlash + 1);
- searchPrefix = pathToComplete.substring(lastSlash + 1);
- } else {
- dirPath = currentPath.endsWith('/') ? currentPath : currentPath + '/';
- searchPrefix = pathToComplete;
- }
- }
-
- // Calculate where to start replacement in the original input
- const spaceBeforeArg = input.lastIndexOf(' ');
- const replaceFrom = spaceBeforeArg >= 0 ? spaceBeforeArg + 1 : 0;
-
- // Navigate to the directory
- const dirParts = dirPath.split('/').filter((part: string) => part !== '');
- let current = fileSystem['/'];
-
- for (const part of dirParts) {
- if (current?.children && current.children[part] && current.children[part].type === 'directory') {
- current = current.children[part];
- } else {
- return { completion: null, replaceFrom };
- }
- }
-
- if (!current?.children) {
- return { completion: null, replaceFrom };
- }
-
- // Get first matching item
- const match = Object.keys(current.children)
- .find(name => name.startsWith(searchPrefix));
-
- if (match) {
- const item = current.children[match];
- const completion = item.type === 'directory' ? `${match}/` : match;
- return { completion, replaceFrom };
- }
- }
-
- return { completion: null, replaceFrom: input.length };
- };
-
- const executeCommand = (input: string): string => {
- const trimmedInput = input.trim();
- if (!trimmedInput) return '';
-
- const [command, ...args] = trimmedInput.split(' ');
-
- switch (command.toLowerCase()) {
- case 'help':
- return `Available commands:
- ls [path] - List directory contents
- cd - Change directory
- cat - Display file contents
- pwd - Show current directory
- clear - Clear terminal
- tree - Show directory structure
- whoami - Display user info
- open - Open page in browser (simulated)
- help - Show this help message
-
- Navigation:
- Use "cd .." to go up one directory
- Use "cd /" to go to root directory
- File paths can be relative or absolute
- Use TAB for auto-completion
-
- Examples:
- ls
- cd resume
- cat about.txt
- open /resume`;
-
- case 'ls': {
- const targetPath = args[0] ? resolvePath(args[0]) : currentPath;
- const pathParts = targetPath.split('/').filter((part: string) => part !== '');
- let target = fileSystem['/'];
-
- for (const part of pathParts) {
- if (target?.children && target.children[part] && target.children[part].type === 'directory') {
- target = target.children[part];
- } else if (pathParts.length > 0) {
- return `ls: cannot access '${targetPath}': No such file or directory`;
- }
- }
-
- if (!target?.children) {
- return `ls: cannot access '${targetPath}': Not a directory`;
- }
-
- const items = Object.values(target.children)
- .map(item => {
- const color = item.type === 'directory' ? '\x1b[34m' : '\x1b[0m';
- const suffix = item.type === 'directory' ? '/' : '';
- return `${color}${item.name}${suffix}\x1b[0m`;
- })
- .join(' ');
-
- return items || 'Directory is empty';
- }
-
- case 'cd': {
- const targetPath = args[0] ? resolvePath(args[0]) : '/';
- const pathParts = targetPath.split('/').filter((part: string) => part !== '');
- let current = fileSystem['/'];
-
- for (const part of pathParts) {
- if (current?.children && current.children[part] && current.children[part].type === 'directory') {
- current = current.children[part];
- } else {
- return `cd: no such file or directory: ${targetPath}`;
- }
- }
-
- setCurrentPath(targetPath || '/');
- return '';
- }
-
- case 'pwd':
- return currentPath || '/';
-
- case 'cat': {
- if (!args[0]) {
- return 'cat: missing file argument';
- }
-
- const filePath = resolvePath(args[0]);
- const pathParts = filePath.split('/').filter((part: string) => part !== '');
- const fileName = pathParts.pop();
-
- let current = fileSystem['/'];
- for (const part of pathParts) {
- if (current?.children && current.children[part] && current.children[part].type === 'directory') {
- current = current.children[part];
- } else {
- return `cat: ${filePath}: No such file or directory`;
- }
- }
-
- if (!fileName || !current?.children || !current.children[fileName]) {
- return `cat: ${filePath}: No such file or directory`;
- }
-
- const file = current.children[fileName];
- if (file.type !== 'file') {
- return `cat: ${filePath}: Is a directory`;
- }
-
- return file.content || '';
- }
-
- case 'tree': {
- const buildTree = (node: FileSystemNode, prefix: string = '', isLast: boolean = true): string => {
- let result = '';
- if (!node.children) return result;
-
- const entries = Object.entries(node.children);
- entries.forEach(([name, child], index) => {
- const isLastChild = index === entries.length - 1;
- const connector = isLastChild ? '└── ' : '├── ';
- const color = child.type === 'directory' ? '\x1b[34m' : '\x1b[0m';
- const suffix = child.type === 'directory' ? '/' : '';
-
- result += `${prefix}${connector}${color}${name}${suffix}\x1b[0m\n`;
-
- if (child.type === 'directory') {
- const newPrefix = prefix + (isLastChild ? ' ' : '│ ');
- result += buildTree(child, newPrefix, isLastChild);
- }
- });
-
- return result;
- };
-
- return '.\n' + buildTree(fileSystem['/']);
- }
-
- case 'clear':
- setCommandHistory([]);
- return '';
-
- case 'whoami':
- return 'guest@atri.dad';
-
- case 'open': {
- const path = args[0];
- if (!path) {
- return 'open: missing path argument';
- }
-
- let url = '';
- if (path === '/resume' || path.startsWith('/resume')) {
- url = '/resume';
- } else if (path === '/projects' || path.startsWith('/projects')) {
- url = '/projects';
- } else if (path === '/posts' || path.startsWith('/posts')) {
- url = '/posts';
- } else if (path === '/' || path === '/about.txt') {
- url = '/';
- } else {
- return `open: cannot open '${path}': No associated page`;
- }
-
- window.open(url, '_blank');
- return `Opening ${url} in new tab...`;
- }
-
- case 'sl':
- setIsTrainRunning(true);
- setTrainPosition(100);
-
- const animateTrain = () => {
- let position = 100;
- const interval = setInterval(() => {
- position -= 1.5;
- setTrainPosition(position);
-
- if (position < -50) {
- clearInterval(interval);
- setIsTrainRunning(false);
- }
- }, 60);
- };
-
- setTimeout(animateTrain, 100);
- return '';
-
- default:
- return `${command}: command not found. Type 'help' for available commands.`;
- }
- };
-
const handleSubmit = (e: JSX.TargetedEvent) => {
e.preventDefault();
-
- const output = executeCommand(currentInput);
+
+ const commandContext: CommandContext = {
+ currentPath,
+ fileSystem,
+ setCurrentPath,
+ setIsTrainRunning,
+ setTrainPosition,
+ };
+
+ const output = executeCommand(currentInput, commandContext);
const newCommand: Command = {
input: currentInput,
output,
timestamp: new Date(),
- path: currentPath
+ path: currentPath,
};
-
+
// Save command to persistent history
- saveCommandToHistory(currentInput);
-
- if (currentInput.trim().toLowerCase() === 'clear') {
+ const updatedHistory = saveCommandToHistory(
+ currentInput,
+ persistentHistory,
+ );
+ setPersistentHistory(updatedHistory);
+
+ if (currentInput.trim().toLowerCase() === "clear") {
setCommandHistory([]);
} else {
setCommandHistory((prev: Command[]) => [...prev, newCommand]);
}
-
- setCurrentInput('');
+
+ setCurrentInput("");
setHistoryIndex(-1);
};
const handleKeyDown = (e: JSX.TargetedKeyboardEvent) => {
- if (e.key === 'Tab') {
+ if (e.key === "Tab") {
e.preventDefault();
-
- const { completion, replaceFrom } = getCompletions(currentInput);
-
+
+ const { completion, replaceFrom } = getCompletions(
+ currentInput,
+ currentPath,
+ fileSystem,
+ );
+
if (completion) {
- // Replace from the correct position with the completion
const beforeReplacement = currentInput.substring(0, replaceFrom);
const newInput = beforeReplacement + completion;
- setCurrentInput(newInput + (completion.endsWith('/') ? '' : ' '));
+ setCurrentInput(newInput + (completion.endsWith("/") ? "" : " "));
}
- } else if (e.key === 'ArrowUp') {
+ } else if (e.key === "ArrowUp") {
e.preventDefault();
if (persistentHistory.length > 0) {
- const newIndex = historyIndex === -1 ? persistentHistory.length - 1 : Math.max(0, historyIndex - 1);
+ const newIndex =
+ historyIndex === -1
+ ? persistentHistory.length - 1
+ : Math.max(0, historyIndex - 1);
setHistoryIndex(newIndex);
setCurrentInput(persistentHistory[newIndex]);
}
- } else if (e.key === 'ArrowDown') {
+ } else if (e.key === "ArrowDown") {
e.preventDefault();
if (historyIndex !== -1) {
- const newIndex = Math.min(persistentHistory.length - 1, historyIndex + 1);
- if (newIndex === persistentHistory.length - 1 && historyIndex === newIndex) {
+ const newIndex = Math.min(
+ persistentHistory.length - 1,
+ historyIndex + 1,
+ );
+ if (
+ newIndex === persistentHistory.length - 1 &&
+ historyIndex === newIndex
+ ) {
setHistoryIndex(-1);
- setCurrentInput('');
+ setCurrentInput("");
} else {
setHistoryIndex(newIndex);
setCurrentInput(persistentHistory[newIndex]);
@@ -604,12 +135,6 @@ ${post.content}`;
}
};
- const formatOutput = (text: string): string => {
- return text
- .replace(/\x1b\[34m/g, '')
- .replace(/\x1b\[0m/g, '');
- };
-
return (
@@ -617,21 +142,23 @@ ${post.content}`;
-
guest@atri.dad: {currentPath}
+
+ guest@atri.dad: {currentPath}
+
-
- inputRef.current?.focus()}
>
{isTrainRunning && (
-
{`
@@ -647,39 +174,49 @@ __/ =| o |=-O=====O=====O=====O \\ ____Y___________|__|_________________________
\\_/ \\__/ \\__/ \\__/ \\__/ \\_/ \\_/ \\_/ \\_/ \\_/`}
)}
-
+
{commandHistory.map((command: Command, index: number) => (
- {command.input && (
-
- guest@atri.dad
- :
- {command.path}
- $
- {command.input}
-
- )}
+ {command.input && (
+
+
+ guest@atri.dad
+
+ :
+
+ {command.path}
+
+ $
+ {command.input}
+
+ )}
{command.output && (
-
)}
))}
-
+
{!isTrainRunning && (