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) { >
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 && (
guest@atri.dad : - {currentPath} + + {currentPath} + $ setCurrentInput((e.target as HTMLInputElement).value)} + onInput={(e) => + setCurrentInput((e.target as HTMLInputElement).value) + } onKeyDown={handleKeyDown} className="flex-1 bg-transparent border-none outline-none text-accent ml-1" autoFocus @@ -693,4 +230,4 @@ __/ =| o |=-O=====O=====O=====O \\ ____Y___________|__|_________________________ ); }; -export default Terminal; \ No newline at end of file +export default Terminal; diff --git a/src/config/data.ts b/src/config/data.ts new file mode 100644 index 0000000..7c31baa --- /dev/null +++ b/src/config/data.ts @@ -0,0 +1,246 @@ +export interface Talk { + id: string; + name: string; + description: string; + link: string; + date?: string; + venue?: string; +} + +export interface Project { + id: string; + name: string; + description: string; + link: string; + technologies?: string[]; + status?: string; +} + +export interface SocialLink { + id: string; + name: string; + url: string; + icon: string; + ariaLabel: string; +} + +export interface TechLink { + id: string; + name: string; + url: string; + icon: string; + ariaLabel: string; +} + +export interface NavigationItem { + id: string; + name: string; + path: string; + tooltip: string; + icon: string; + isActive?: (path: string) => boolean; +} + +export const talks: Talk[] = [ + { + id: "devedmonton-hateoas", + name: "Hypermedia as the engine of application state - An Introduction", + description: "A basic introduction to the concepts behind HATEOAS or Hypermedia as the engine of application state.", + link: "/files/DevEdmonton_Talk_HATEOAS.pdf", + venue: "Dev Edmonton Society", + }, +]; + +export const projects: Project[] = [ + { + id: "bluesky-pds-manager", + name: "BlueSky PDS Manager", + description: "A web-based BlueSky PDS Manager. Manage your invite codes and users with a simple web UI.", + link: "https://pdsman.atri.dad", + }, + { + id: "pollo", + name: "Pollo", + description: "A dead-simple real-time voting tool.", + link: "https://git.atri.dad/atridad/pollo", + }, + { + id: "goth-stack", + name: "GOTH Stack", + description: "🚀 A Web Application Template Powered by HTMX + Go + Tailwind 🚀", + link: "https://git.atri.dad/atridad/goth.stack", + }, + { + id: "himbot", + name: "Himbot", + description: "A discord bot written in Go. Loosly named after my username online (HimbothySwaggins).", + link: "https://git.atri.dad/atridad/himbot", + }, + { + id: "loadr", + name: "loadr", + description: "A lightweight REST load testing tool with robust support for different verbs, token auth, and performance reports.", + link: "https://git.atri.dad/atridad/loadr", + }, +]; + +export const sections = { + resume: { + name: "Resume", + path: "/resume", + description: "Professional experience, skills, and background" + }, + posts: { + name: "Blog Posts", + path: "/posts", + description: "Technical articles and thoughts" + }, + talks: { + name: "Talks", + path: "/talks", + description: "Conference talks and presentations" + }, + projects: { + name: "Projects", + path: "/projects", + description: "Personal and professional projects" + } +} as const; + +export const socialLinks: SocialLink[] = [ + { + id: "email", + name: "Email", + url: "mailto:me@atri.dad", + icon: "mdi:email", + ariaLabel: "Email me" + }, + { + id: "rss", + name: "RSS Feed", + url: "/feed", + icon: "mdi:rss", + ariaLabel: "RSS Feed" + }, + { + id: "gitea", + name: "Forgejo (Git)", + url: "https://git.atri.dad/atridad", + icon: "simple-icons:gitea", + ariaLabel: "Forgejo (Git)" + }, + { + id: "bluesky", + name: "Bluesky", + url: "https://bsky.app/profile/atri.dad", + icon: "simple-icons:bluesky", + ariaLabel: "Bluesky Profile" + }, + { + id: "spotify", + name: "Spotify", + url: "https://open.spotify.com/user/31pjwuuqwnn5zr7fnhfjjmi7c4bi?si=1be2bfdc844c4d85", + icon: "spotify", // Special component + ariaLabel: "Spotify Profile" + } +]; + +export const techLinks: TechLink[] = [ + { + id: "react", + name: "React", + url: "https://react.dev/", + icon: "simple-icons:react", + ariaLabel: "React" + }, + { + id: "typescript", + name: "TypeScript", + url: "https://www.typescriptlang.org/", + icon: "simple-icons:typescript", + ariaLabel: "TypeScript" + }, + { + id: "astro", + name: "Astro", + url: "https://astro.build/", + icon: "simple-icons:astro", + ariaLabel: "Astro" + }, + { + id: "go", + name: "Go", + url: "https://go.dev/", + icon: "simple-icons:go", + ariaLabel: "Go" + }, + { + id: "postgresql", + name: "PostgreSQL", + url: "https://www.postgresql.org/", + icon: "simple-icons:postgresql", + ariaLabel: "PostgreSQL" + }, + { + id: "redis", + name: "Redis", + url: "https://redis.io/", + icon: "simple-icons:redis", + ariaLabel: "Redis" + }, + { + id: "docker", + name: "Docker", + url: "https://www.docker.com/", + icon: "simple-icons:docker", + ariaLabel: "Docker" + } +]; + +export const navigationItems: NavigationItem[] = [ + { + id: "home", + name: "Home", + path: "/", + tooltip: "Home", + icon: "Home" + }, + { + id: "posts", + name: "Posts", + path: "/posts", + tooltip: "Posts", + icon: "NotebookPen", + isActive: (path: string) => path.startsWith("/posts") || path.startsWith("/post/") + }, + { + id: "resume", + name: "Resume", + path: "/resume", + tooltip: "Resume", + icon: "BriefcaseBusiness" + }, + { + id: "projects", + name: "Projects", + path: "/projects", + tooltip: "Projects", + icon: "CodeXml", + isActive: (path: string) => path.startsWith("/projects") + }, + { + id: "talks", + name: "Talks", + path: "/talks", + tooltip: "Talks", + icon: "Megaphone", + isActive: (path: string) => path.startsWith("/talks") + }, + { + id: "terminal", + name: "Terminal", + path: "/terminal", + tooltip: "Terminal", + icon: "TerminalIcon" + } +]; \ No newline at end of file diff --git a/src/lib/terminal/commands.ts b/src/lib/terminal/commands.ts new file mode 100644 index 0000000..6648e2c --- /dev/null +++ b/src/lib/terminal/commands.ts @@ -0,0 +1,234 @@ +import type { FileSystemNode } from './types'; +import { getCurrentDirectory, resolvePath } from './fileSystem'; + +export interface CommandContext { + currentPath: string; + fileSystem: { [key: string]: FileSystemNode }; + setCurrentPath: (path: string) => void; + setIsTrainRunning: (running: boolean) => void; + setTrainPosition: (position: number) => void; +} + +export function executeCommand(input: string, context: CommandContext): string { + const trimmedInput = input.trim(); + if (!trimmedInput) return ''; + + const [command, ...args] = trimmedInput.split(' '); + + switch (command.toLowerCase()) { + case 'help': + return handleHelp(); + case 'ls': + return handleLs(args, context); + case 'cd': + return handleCd(args, context); + case 'pwd': + return handlePwd(context); + case 'cat': + return handleCat(args, context); + case 'tree': + return handleTree(context); + case 'clear': + return ''; + case 'whoami': + return handleWhoami(); + case 'open': + return handleOpen(args); + case 'sl': + return handleSl(context); + default: + return `${command}: command not found. Type 'help' for available commands.`; + } +} + +function handleHelp(): string { + 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 + open /talks + cat /talks/README.txt + cd social + cat /tech/README.txt`; +} + +function handleLs(args: string[], context: CommandContext): string { + const { currentPath, fileSystem } = context; + const targetPath = args[0] ? resolvePath(currentPath, 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'; +} + +function handleCd(args: string[], context: CommandContext): string { + const { currentPath, fileSystem, setCurrentPath } = context; + const targetPath = args[0] ? resolvePath(currentPath, 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 ''; +} + +function handlePwd(context: CommandContext): string { + return context.currentPath || '/'; +} + +function handleCat(args: string[], context: CommandContext): string { + const { currentPath, fileSystem } = context; + + if (!args[0]) { + return 'cat: missing file argument'; + } + + const filePath = resolvePath(currentPath, 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 || ''; +} + +function handleTree(context: CommandContext): string { + const { fileSystem } = context; + + 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['/']); +} + +function handleWhoami(): string { + return 'guest@atri.dad'; +} + +function handleOpen(args: string[]): string { + 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 === '/talks' || path.startsWith('/talks')) { + url = '/talks'; + } 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...`; +} + +function handleSl(context: CommandContext): string { + const { setIsTrainRunning, setTrainPosition } = context; + + 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 ''; +} \ No newline at end of file diff --git a/src/lib/terminal/fileSystem.ts b/src/lib/terminal/fileSystem.ts new file mode 100644 index 0000000..626a2a9 --- /dev/null +++ b/src/lib/terminal/fileSystem.ts @@ -0,0 +1,367 @@ +import type { FileSystemNode, ResumeData } from './types'; +import { talks, projects, socialLinks, techLinks } from '../../config/data'; + +export async function buildFileSystem(): Promise<{ [key: string]: FileSystemNode }> { + 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 rxresume json + const resumeFiles = buildResumeFiles(resumeData); + const postsFiles = buildPostsFiles(postsData); + const talksFiles = buildTalksFiles(); + const projectsFiles = buildProjectsFiles(); + const socialFiles = buildSocialFiles(); + const techFiles = buildTechFiles(); + const contactContent = buildContactContent(resumeData); + + 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\nExplore the directories:\n- /resume - Professional experience and skills\n- /posts - Blog posts and articles\n- /talks - Conference presentations\n- /projects - Personal and professional projects\n- /social - Social media and contact links\n- /tech - Technologies and tools I use\n\nType "ls" to see all available files and directories.` + }, + 'resume': { + type: 'directory', + name: 'resume', + children: resumeFiles + }, + 'posts': { + type: 'directory', + name: 'posts', + children: postsFiles + }, + 'talks': { + type: 'directory', + name: 'talks', + children: talksFiles + }, + 'projects': { + type: 'directory', + name: 'projects', + children: projectsFiles + }, + 'social': { + type: 'directory', + name: 'social', + children: socialFiles + }, + 'tech': { + type: 'directory', + name: 'tech', + children: techFiles + }, + 'contact.txt': { + type: 'file', + name: 'contact.txt', + content: contactContent + } + } + } + }; + + return fs; + } catch (error) { + console.error('Error loading resume data:', error); + return buildFallbackFileSystem(); + } +} + +function buildResumeFiles(resumeData: ResumeData): { [key: string]: FileSystemNode } { + 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 + }; + } + + return resumeFiles; +} + +function buildPostsFiles(postsData: any[]): { [key: string]: FileSystemNode } { + 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') + } + }; + + 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 + }; + }); + + return postsFiles; +} + +function buildTalksFiles(): { [key: string]: FileSystemNode } { + const talksFiles: { [key: string]: FileSystemNode } = { + 'README.txt': { + type: 'file', + name: 'README.txt', + content: 'Conference talks and presentations.\n\nUse "open /talks" to see the full list on the website.\n\nAvailable talks:\n' + + talks.map(talk => `- ${talk.id}.md`).join('\n') + } + }; + + talks.forEach(talk => { + const fileName = `${talk.id}.md`; + let content = `--- +title: "${talk.name}" +description: "${talk.description}" +${talk.venue ? `venue: "${talk.venue}"` : ''} +${talk.date ? `date: "${talk.date}"` : ''} +link: "${talk.link}" +--- + +# ${talk.name} + +${talk.description} + +${talk.venue ? `**Venue:** ${talk.venue}` : ''} +${talk.date ? `**Date:** ${talk.date}` : ''} + +**Download:** [${talk.link}](${talk.link})`; + + talksFiles[fileName] = { + type: 'file', + name: fileName, + content + }; + }); + + return talksFiles; +} + +function buildProjectsFiles(): { [key: string]: FileSystemNode } { + const projectsFiles: { [key: string]: FileSystemNode } = { + 'README.txt': { + type: 'file', + name: 'README.txt', + content: 'Personal and professional projects.\n\nUse "open /projects" to see the full portfolio on the website.\n\nAvailable projects:\n' + + projects.map(project => `- ${project.id}.md`).join('\n') + } + }; + + projects.forEach(project => { + const fileName = `${project.id}.md`; + let content = `--- +title: "${project.name}" +description: "${project.description}" +${project.status ? `status: "${project.status}"` : ''} +${project.link ? `link: "${project.link}"` : ''} +${project.technologies ? `technologies: [${project.technologies.map(tech => `"${tech}"`).join(', ')}]` : ''} +--- + +# ${project.name} + +${project.description} + +${project.status ? `**Status:** ${project.status}` : ''} +${project.technologies ? `**Technologies:** ${project.technologies.join(', ')}` : ''} +${project.link ? `**Link:** [${project.link}](${project.link})` : ''}`; + + projectsFiles[fileName] = { + type: 'file', + name: fileName, + content + }; + }); + + return projectsFiles; +} + +function buildSocialFiles(): { [key: string]: FileSystemNode } { + const socialFiles: { [key: string]: FileSystemNode } = { + 'README.txt': { + type: 'file', + name: 'README.txt', + content: 'Social media profiles and contact information.\n\nAvailable social links:\n' + + socialLinks.map(link => `- ${link.id}.txt`).join('\n') + } + }; + + socialLinks.forEach(link => { + const fileName = `${link.id}.txt`; + let content = `${link.name} + +${link.ariaLabel} + +URL: ${link.url} +Icon: ${link.icon}`; + + socialFiles[fileName] = { + type: 'file', + name: fileName, + content + }; + }); + + return socialFiles; +} + +function buildTechFiles(): { [key: string]: FileSystemNode } { + const techFiles: { [key: string]: FileSystemNode } = { + 'README.txt': { + type: 'file', + name: 'README.txt', + content: 'Technologies and tools I use.\n\nAvailable tech links:\n' + + techLinks.map(link => `- ${link.id}.txt`).join('\n') + } + }; + + techLinks.forEach(link => { + const fileName = `${link.id}.txt`; + let content = `${link.name} + +${link.ariaLabel} + +URL: ${link.url} +Icon: ${link.icon}`; + + techFiles[fileName] = { + type: 'file', + name: fileName, + content + }; + }); + + return techFiles; +} + +function buildContactContent(resumeData: ResumeData): string { + return [ + `Email: ${resumeData.basics.email}`, + '', + 'Social Profiles:', + ...resumeData.sections.profiles.items.map(profile => + `${profile.network}: ${profile.url.href}` + ) + ].join('\n'); +} + +function buildFallbackFileSystem(): { [key: string]: FileSystemNode } { + return { + '/': { + 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.' + } + } + } + }; +} + +export function getCurrentDirectory(fileSystem: { [key: string]: FileSystemNode }, currentPath: string): 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; +} + +export function resolvePath(currentPath: string, 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('/'); +} \ No newline at end of file diff --git a/src/lib/terminal/types.ts b/src/lib/terminal/types.ts new file mode 100644 index 0000000..840152a --- /dev/null +++ b/src/lib/terminal/types.ts @@ -0,0 +1,40 @@ +export interface FileSystemNode { + type: 'directory' | 'file'; + name: string; + content?: string; + children?: { [key: string]: FileSystemNode }; +} + +export 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 }[] }; + }; +} + +export interface Command { + input: string; + output: string; + timestamp: Date; + path: string; +} + +export interface TerminalState { + currentPath: string; + commandHistory: Command[]; + currentInput: string; + historyIndex: number; + fileSystem: { [key: string]: FileSystemNode }; + isTrainRunning: boolean; + trainPosition: number; + persistentHistory: string[]; +} \ No newline at end of file diff --git a/src/lib/terminal/utils.ts b/src/lib/terminal/utils.ts new file mode 100644 index 0000000..0922d49 --- /dev/null +++ b/src/lib/terminal/utils.ts @@ -0,0 +1,98 @@ +import type { FileSystemNode } from './types'; +import { resolvePath } from './fileSystem'; + +export function getCompletions( + input: string, + currentPath: string, + fileSystem: { [key: string]: FileSystemNode } +): { 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 + if (parts.length > 1 && ['ls', 'cd', 'cat', 'open'].includes(command)) { + // Path completion + const isAbsolute = partialPath.startsWith('/'); + const pathToComplete = isAbsolute ? partialPath : resolvePath(currentPath, 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 }; +} + +export function formatOutput(text: string): string { + return text + .replace(/\x1b\[34m/g, '') + .replace(/\x1b\[0m/g, ''); +} + +export function saveCommandToHistory(command: string, persistentHistory: string[]): string[] { + if (command.trim()) { + const updatedHistory = [...persistentHistory, command].slice(-100); // Keep last 100 commands + localStorage.setItem('terminal-history', JSON.stringify(updatedHistory)); + return updatedHistory; + } + return persistentHistory; +} + +export function loadCommandHistory(): string[] { + const savedHistory = localStorage.getItem('terminal-history'); + if (savedHistory) { + try { + return JSON.parse(savedHistory); + } catch (error) { + console.error('Error loading command history:', error); + } + } + return []; +} \ No newline at end of file diff --git a/src/pages/projects.astro b/src/pages/projects.astro index e46c99a..1ac5454 100644 --- a/src/pages/projects.astro +++ b/src/pages/projects.astro @@ -1,43 +1,7 @@ --- import Layout from "../layouts/Layout.astro"; import ProjectCard from "../components/ProjectCard.astro"; - -const projects = [ - { - id: "bluesky-pds-manager", - name: "BlueSky PDS Manager", - description: - "A web-based BlueSky PDS Manager. Manage your invite codes and users with a simple web UI.", - link: "https://pdsman.atri.dad", - }, - { - id: "pollo", - name: "Pollo", - description: "A dead-simple real-time voting tool.", - link: "https://git.atri.dad/atridad/pollo", - }, - { - id: "goth-stack", - name: "GOTH Stack", - description: - "🚀 A Web Application Template Powered by HTMX + Go + Tailwind 🚀", - link: "https://git.atri.dad/atridad/goth.stack", - }, - { - id: "himbot", - name: "Himbot", - description: - "A discord bot written in Go. Loosly named after my username online (HimbothySwaggins).", - link: "https://git.atri.dad/atridad/himbot", - }, - { - id: "loadr", - name: "loadr", - description: - "A lightweight REST load testing tool with robust support for different verbs, token auth, and performance reports.", - link: "https://git.atri.dad/atridad/loadr", - }, -]; +import { projects } from "../config/data"; --- diff --git a/src/pages/talks.astro b/src/pages/talks.astro index 44c8d10..3bd278e 100644 --- a/src/pages/talks.astro +++ b/src/pages/talks.astro @@ -1,16 +1,7 @@ --- import Layout from "../layouts/Layout.astro"; import TalkCard from "../components/TalkCard.astro"; - -const talks = [ - { - id: "devedmonton-hateoas", - name: "Hypermedia as the engine of application state - An Introduction", - description: - "A basic introduction to the concepts behind HATEOAS or Hypermedia as the engine of application state.", - link: "/files/DevEdmonton_Talk_HATEOAS.pdf", - }, -]; +import { talks } from "../config/data"; ---