Fixed term!
Some checks failed
Docker Deploy / build-and-push (push) Has been cancelled

This commit is contained in:
2025-06-26 16:19:52 -06:00
parent f89d32d6ce
commit 43c96c73b2
7 changed files with 534 additions and 160 deletions

164
README.md
View File

@ -1 +1,163 @@
# Personal Site # Personal Website
My personal website built with Astro and Preact!
## Features
- **Resume Management**
- **Blog Posts**
- **Projects**
- **Talks & Presentations**
- **Terminal View**
## Resume Configuration
The resume system supports multiple sections that can be enabled, disabled, and customized.
### Available Resume Sections
| Section | Required Fields |
|---------|-----------------|
| **basics** | `name`, `email`, `profiles` |
| **summary** | `content` |
| **experience** | `company`, `position`, `location`, `date`, `description` |
| **education** | `institution`, `degree`, `field`, `date` |
| **skills** | `name`, `level` (1-5) |
| **volunteer** | `organization`, `position`, `date` |
| **awards** | `title`, `organization`, `date` |
| **profiles** | `network`, `username`, `url` |
### Section Configuration
Each section can be configured in `src/config/data.ts`:
```typescript
export const resumeConfig: ResumeConfig = {
tomlFile: "/files/resume.toml",
sections: {
summary: {
title: "Professional Summary",
enabled: true,
},
experience: {
title: "Work Experience",
enabled: true,
},
awards: {
title: "Awards & Recognition",
enabled: true,
},
// ... other sections
},
};
```
### Resume Data Format (TOML)
Create a `resume.toml` file in the `public/files/` directory:
```toml
[basics]
name = "Your Name"
email = "your.email@example.com"
[[basics.profiles]]
network = "GitHub"
username = "yourusername"
url = "https://github.com/yourusername"
[[basics.profiles]]
network = "LinkedIn"
username = "yourname"
url = "https://linkedin.com/in/yourname"
[summary]
content = "Your professional summary here..."
[[experience]]
company = "Company Name"
position = "Job Title"
location = "City, State"
date = "2020 - Present"
description = [
"Achievement or responsibility 1",
"Achievement or responsibility 2"
]
url = "https://company.com"
[[education]]
institution = "University Name"
degree = "Bachelor of Science"
field = "Computer Science"
date = "2016 - 2020"
details = [
"Relevant coursework or achievements"
]
[[skills]]
name = "JavaScript"
level = 4
[[skills]]
name = "Python"
level = 5
[[volunteer]]
organization = "Organization Name"
position = "Volunteer Position"
date = "2019 - Present"
[[awards]]
title = "Award Title"
organization = "Awarding Organization"
date = "2023"
description = "Brief description of the award"
```
### Section Field Details
#### Skills Section
- `level`: Integer from 1-5 representing proficiency level
- Displays as progress bars with visual indicators
#### Experience Section
- `description`: Array of strings for bullet points
- `url`: Optional company website link
#### Education Section
- `details`: Optional array of additional information (coursework, achievements, etc.)
#### Awards Section
- `description`: Optional additional details about the award
#### Profiles Section
- `network`: Used for icon selection (GitHub, LinkedIn, etc.)
- Icons automatically selected based on network name
## Usage
1. **Configure Resume**: Edit `src/config/data.ts` to enable/disable sections
2. **Add Resume Data**: Create `public/files/resume.toml` with your information
3. **View Resume**: Navigate to `/resume` on your site
4. **Generate PDF**: Click "Generate PDF Resume" button for downloadable PDF
## Development
```bash
# Install dependencies
npm install
# Start development server
npm run dev
# Build for production
npm run build
```
## Resume PDF Generation
The system automatically generates PDFs using Puppeteer with:
- Optimized layout for A4 paper
- Print-friendly styling
- Consistent formatting across sections
- Proper page breaks and margins

View File

@ -62,7 +62,7 @@ export const homepageSections: HomepageSections = {
// Resume Configuration // Resume Configuration
export const resumeConfig: ResumeConfig = { export const resumeConfig: ResumeConfig = {
jsonFile: "/files/resume.toml", tomlFile: "/files/resume.toml",
pdfFile: { pdfFile: {
path: "/files/Atridad_Lahiji_Resume.pdf", path: "/files/Atridad_Lahiji_Resume.pdf",
filename: "Atridad_Lahiji_Resume.pdf", filename: "Atridad_Lahiji_Resume.pdf",
@ -76,6 +76,7 @@ export const resumeConfig: ResumeConfig = {
"skills", "skills",
"volunteer", "volunteer",
"profiles", "profiles",
"awards",
], ],
summary: { summary: {
title: "Summary", title: "Summary",
@ -101,6 +102,10 @@ export const resumeConfig: ResumeConfig = {
title: "Professional Profiles", title: "Professional Profiles",
enabled: true, enabled: true,
}, },
awards: {
title: "Awards & Recognition",
enabled: true,
},
}, },
}; };
@ -303,7 +308,7 @@ export const navigationItems: NavigationItem[] = [
path: "/resume", path: "/resume",
tooltip: "Resume", tooltip: "Resume",
icon: BriefcaseBusiness, icon: BriefcaseBusiness,
enabled: !!(resumeConfig.jsonFile && resumeConfig.jsonFile.trim()), enabled: !!(resumeConfig.tomlFile && resumeConfig.tomlFile.trim()),
}, },
{ {
id: "projects", id: "projects",

View File

@ -0,0 +1,37 @@
import type { APIRoute } from "astro";
import * as TOML from "@iarna/toml";
import { siteConfig } from "../../config/data";
export const GET: APIRoute = async ({ request }) => {
try {
// Check if resume TOML file is configured
if (!siteConfig.resume.tomlFile || !siteConfig.resume.tomlFile.trim()) {
return new Response("Resume not configured", { status: 404 });
}
const url = new URL(request.url);
const baseUrl = `${url.protocol}//${url.host}`;
// Fetch the TOML file from the public directory
const response = await fetch(`${baseUrl}${siteConfig.resume.tomlFile}`);
if (!response.ok) {
throw new Error(
`Failed to fetch resume TOML: ${response.status} ${response.statusText}`,
);
}
const tomlContent = await response.text();
const resumeData = TOML.parse(tomlContent);
return new Response(JSON.stringify(resumeData), {
headers: {
"Content-Type": "application/json",
"Cache-Control": "public, max-age=300", // Cache for 5 minutes
},
});
} catch (error) {
console.error("Error parsing resume TOML:", error);
return new Response("Error parsing resume data", { status: 500 });
}
};

View File

@ -41,6 +41,12 @@ interface ResumeData {
position: string; position: string;
date: string; date: string;
}[]; }[];
awards: {
title: string;
organization: string;
date: string;
description?: string;
}[];
} }
const generateResumeHTML = (data: ResumeData): string => { const generateResumeHTML = (data: ResumeData): string => {
@ -128,6 +134,23 @@ const generateResumeHTML = (data: ResumeData): string => {
}) })
.join("") || ""; .join("") || "";
const awardsHTML =
data.awards
?.map((award) => {
return `
<div class="mb-2 pl-2 border-l-2 border-yellow-600">
<h3 class="text-xs font-semibold text-gray-900 mb-1">${award.title}</h3>
<div class="text-xs text-gray-600 mb-1">
<span class="font-medium">${award.organization}</span>
<span class="mx-1">•</span>
<span>${award.date}</span>
</div>
${award.description ? `<div class="text-xs text-gray-700 leading-tight">${award.description}</div>` : ""}
</div>
`;
})
.join("") || "";
return ` return `
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
@ -217,6 +240,23 @@ const generateResumeHTML = (data: ResumeData): string => {
` `
: "" : ""
} }
${
data.awards &&
data.awards.length > 0 &&
resumeConfig.sections.awards?.enabled
? `
<section>
<h2 class="text-sm font-semibold text-gray-900 mb-2 pb-1 border-b border-gray-300">
${resumeConfig.sections.awards.title || "Awards & Recognition"}
</h2>
<div class="space-y-2">
${awardsHTML}
</div>
</section>
`
: ""
}
</div> </div>
<div class="space-y-4"> <div class="space-y-4">
@ -263,14 +303,14 @@ const generateResumeHTML = (data: ResumeData): string => {
export const GET: APIRoute = async ({ request }) => { export const GET: APIRoute = async ({ request }) => {
try { try {
if (!siteConfig.resume.jsonFile || !siteConfig.resume.jsonFile.trim()) { if (!siteConfig.resume.tomlFile || !siteConfig.resume.tomlFile.trim()) {
return new Response("Resume not configured", { status: 404 }); return new Response("Resume not configured", { status: 404 });
} }
const url = new URL(request.url); const url = new URL(request.url);
const baseUrl = `${url.protocol}//${url.host}`; const baseUrl = `${url.protocol}//${url.host}`;
const response = await fetch(`${baseUrl}${siteConfig.resume.jsonFile}`); const response = await fetch(`${baseUrl}${siteConfig.resume.tomlFile}`);
if (!response.ok) { if (!response.ok) {
throw new Error( throw new Error(
@ -279,7 +319,9 @@ export const GET: APIRoute = async ({ request }) => {
} }
const tomlContent = await response.text(); const tomlContent = await response.text();
const resumeData: ResumeData = TOML.parse(tomlContent) as unknown as ResumeData; const resumeData: ResumeData = TOML.parse(
tomlContent,
) as unknown as ResumeData;
const htmlContent = generateResumeHTML(resumeData); const htmlContent = generateResumeHTML(resumeData);

View File

@ -45,13 +45,19 @@ interface ResumeData {
position: string; position: string;
date: string; date: string;
}[]; }[];
awards: {
title: string;
organization: string;
date: string;
description?: string;
}[];
} }
let resumeData: ResumeData | undefined = undefined; let resumeData: ResumeData | undefined = undefined;
let fetchError: string | null = null; let fetchError: string | null = null;
// Check if resume JSON file is configured before attempting to fetch // Check if resume TOML file is configured before attempting to fetch
if (!siteConfig.resume.jsonFile || !siteConfig.resume.jsonFile.trim()) { if (!siteConfig.resume.tomlFile || !siteConfig.resume.tomlFile.trim()) {
return Astro.redirect("/"); return Astro.redirect("/");
} }
@ -60,7 +66,7 @@ try {
const baseUrl = Astro.url.origin; const baseUrl = Astro.url.origin;
// Fetch the TOML file from the public directory // Fetch the TOML file from the public directory
const response = await fetch(`${baseUrl}${siteConfig.resume.jsonFile}`); const response = await fetch(`${baseUrl}${siteConfig.resume.tomlFile}`);
if (!response.ok) { if (!response.ok) {
throw new Error( throw new Error(
@ -331,5 +337,42 @@ if (!data) {
</div> </div>
) )
} }
{
data.awards &&
data.awards.length > 0 &&
resumeConfig.sections.awards?.enabled && (
<div class="card bg-base-200 shadow-xl mb-4 sm:mb-6">
<div class="card-body p-4 sm:p-6 break-words">
<h2 class="card-title text-xl sm:text-2xl">
{resumeConfig.sections.awards.title ||
"Awards & Recognition"}
</h2>
<div class="space-y-4">
{data.awards.map((award) => (
<div class="border-l-2 border-warning pl-4 sm:pl-6">
<h3 class="text-lg sm:text-xl font-semibold">
{award.title}
</h3>
<div class="text-sm sm:text-base text-base-content/70 mb-2">
<span class="font-medium">
{award.organization}
</span>
<span class="block sm:inline sm:ml-4">
{award.date}
</span>
</div>
{award.description && (
<div class="text-sm sm:text-base text-base-content/80 mt-2">
{award.description}
</div>
)}
</div>
))}
</div>
</div>
</div>
)
}
</div> </div>
</Layout> </Layout>

View File

@ -51,7 +51,7 @@ export interface NavigationItem {
} }
export interface ResumeConfig { export interface ResumeConfig {
jsonFile: string; tomlFile: string;
pdfFile: { pdfFile: {
path: string; path: string;
filename: string; filename: string;
@ -83,6 +83,10 @@ export interface ResumeConfig {
title?: string; title?: string;
enabled?: boolean; enabled?: boolean;
}; };
awards?: {
title?: string;
enabled?: boolean;
};
}; };
} }

View File

@ -1,18 +1,27 @@
import type { FileSystemNode, ResumeData } from './types'; import type { FileSystemNode, ResumeData } from "./types";
import { talks, projects, socialLinks, techLinks } from '../../config/data'; import { talks, projects, socialLinks, techLinks } from "../../config/data";
export async function buildFileSystem(): Promise<{ [key: string]: FileSystemNode }> { export async function buildFileSystem(): Promise<{
[key: string]: FileSystemNode;
}> {
try { try {
const response = await fetch('/files/resume.json'); const response = await fetch("/api/resume.json");
const resumeData: ResumeData = await response.json();
if (!response.ok) {
throw new Error(
`Failed to fetch resume data: ${response.status} ${response.statusText}`,
);
}
const resumeData: any = await response.json();
// Fetch blog posts // Fetch blog posts
const postsResponse = await fetch('/api/posts.json'); const postsResponse = await fetch("/api/posts.json");
let postsData = []; let postsData = [];
try { try {
postsData = await postsResponse.json(); postsData = await postsResponse.json();
} catch (error) { } catch (error) {
console.log('Could not fetch posts data:', error); console.log("Could not fetch posts data:", error);
} }
// Build resume files from rxresume json // Build resume files from rxresume json
@ -25,117 +34,140 @@ export async function buildFileSystem(): Promise<{ [key: string]: FileSystemNode
const contactContent = buildContactContent(resumeData); const contactContent = buildContactContent(resumeData);
const fs: { [key: string]: FileSystemNode } = { const fs: { [key: string]: FileSystemNode } = {
'/': { "/": {
type: 'directory', type: "directory",
name: '/', name: "/",
children: { children: {
'about.txt': { "about.txt": {
type: 'file', type: "file",
name: 'about.txt', 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.` 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': { resume: {
type: 'directory', type: "directory",
name: 'resume', name: "resume",
children: resumeFiles children: resumeFiles,
}, },
'posts': { posts: {
type: 'directory', type: "directory",
name: 'posts', name: "posts",
children: postsFiles children: postsFiles,
}, },
'talks': { talks: {
type: 'directory', type: "directory",
name: 'talks', name: "talks",
children: talksFiles children: talksFiles,
}, },
'projects': { projects: {
type: 'directory', type: "directory",
name: 'projects', name: "projects",
children: projectsFiles children: projectsFiles,
}, },
'social': { social: {
type: 'directory', type: "directory",
name: 'social', name: "social",
children: socialFiles children: socialFiles,
}, },
'tech': { tech: {
type: 'directory', type: "directory",
name: 'tech', name: "tech",
children: techFiles children: techFiles,
}, },
'contact.txt': { "contact.txt": {
type: 'file', type: "file",
name: 'contact.txt', name: "contact.txt",
content: contactContent content: contactContent,
} },
} },
} },
}; };
return fs; return fs;
} catch (error) { } catch (error) {
console.error('Error loading resume data:', error); console.error("Error loading resume data:", error);
return buildFallbackFileSystem(); return buildFallbackFileSystem();
} }
} }
function buildResumeFiles(resumeData: ResumeData): { [key: string]: FileSystemNode } { function buildResumeFiles(resumeData: any): { [key: string]: FileSystemNode } {
const resumeFiles: { [key: string]: FileSystemNode } = {}; const resumeFiles: { [key: string]: FileSystemNode } = {};
if (resumeData.sections.summary) { try {
resumeFiles['summary.txt'] = { if (resumeData.summary) {
type: 'file', resumeFiles["summary.txt"] = {
name: 'summary.txt', type: "file",
content: resumeData.sections.summary.content.replace(/<[^>]*>/g, '') name: "summary.txt",
}; content: resumeData.summary.content,
} };
}
if (resumeData.sections.skills?.items) { if (resumeData.skills && Array.isArray(resumeData.skills)) {
const skillsContent = resumeData.sections.skills.items const skillsContent = resumeData.skills
.map(skill => `${skill.name} (Level: ${skill.level}/5)`) .map((skill: any) => `${skill.name} (Level: ${skill.level}/5)`)
.join('\n'); .join("\n");
resumeFiles['skills.txt'] = { resumeFiles["skills.txt"] = {
type: 'file', type: "file",
name: 'skills.txt', name: "skills.txt",
content: skillsContent content: skillsContent,
}; };
} }
if (resumeData.sections.experience?.items) { if (resumeData.experience && Array.isArray(resumeData.experience)) {
const experienceContent = resumeData.sections.experience.items const experienceContent = resumeData.experience
.map(exp => { .map((exp: any) => {
const summary = exp.summary.replace(/<[^>]*>/g, '').replace(/&nbsp;/g, ' '); const description = Array.isArray(exp.description)
return `${exp.position} at ${exp.company}\n${exp.date} | ${exp.location}\n${summary}\n${exp.url?.href ? `URL: ${exp.url.href}` : ''}\n`; ? exp.description.join("\n• ")
}) : "";
.join('\n---\n\n'); return `${exp.position} at ${exp.company}\n${exp.date} | ${exp.location}\n• ${description}\n${exp.url ? `URL: ${exp.url}` : ""}\n`;
resumeFiles['experience.txt'] = { })
type: 'file', .join("\n---\n\n");
name: 'experience.txt', resumeFiles["experience.txt"] = {
content: experienceContent type: "file",
}; name: "experience.txt",
} content: experienceContent,
};
}
if (resumeData.sections.education?.items) { if (resumeData.education && Array.isArray(resumeData.education)) {
const educationContent = resumeData.sections.education.items const educationContent = resumeData.education
.map(edu => `${edu.institution}\n${edu.studyType} - ${edu.area}\n${edu.date}\n${edu.summary ? edu.summary.replace(/<[^>]*>/g, '') : ''}`) .map(
.join('\n\n---\n\n'); (edu: any) =>
resumeFiles['education.txt'] = { `${edu.institution}\n${edu.degree} - ${edu.field}\n${edu.date}\n${edu.details && Array.isArray(edu.details) ? edu.details.join("\n• ") : ""}`,
type: 'file', )
name: 'education.txt', .join("\n\n---\n\n");
content: educationContent resumeFiles["education.txt"] = {
}; type: "file",
} name: "education.txt",
content: educationContent,
};
}
if (resumeData.sections.volunteer?.items) { if (resumeData.volunteer && Array.isArray(resumeData.volunteer)) {
const volunteerContent = resumeData.sections.volunteer.items const volunteerContent = resumeData.volunteer
.map(vol => `${vol.organization}\n${vol.position}\n${vol.date}`) .map((vol: any) => `${vol.organization}\n${vol.position}\n${vol.date}`)
.join('\n\n---\n\n'); .join("\n\n---\n\n");
resumeFiles['volunteer.txt'] = { resumeFiles["volunteer.txt"] = {
type: 'file', type: "file",
name: 'volunteer.txt', name: "volunteer.txt",
content: volunteerContent content: volunteerContent,
}; };
}
if (resumeData.awards && Array.isArray(resumeData.awards)) {
const awardsContent = resumeData.awards
.map(
(award: any) =>
`${award.title}\n${award.organization}\n${award.date}\n${award.description || ""}`,
)
.join("\n\n---\n\n");
resumeFiles["awards.txt"] = {
type: "file",
name: "awards.txt",
content: awardsContent,
};
}
} catch (error) {
console.error("Error building resume files:", error);
} }
return resumeFiles; return resumeFiles;
@ -150,15 +182,15 @@ function buildPostsFiles(postsData: any[]): { [key: string]: FileSystemNode } {
title: "${post.title}" title: "${post.title}"
description: "${post.description}" description: "${post.description}"
pubDate: "${post.pubDate}" pubDate: "${post.pubDate}"
tags: [${post.tags.map((tag: string) => `"${tag}"`).join(', ')}] tags: [${post.tags.map((tag: string) => `"${tag}"`).join(", ")}]
--- ---
${post.content}`; ${post.content}`;
postsFiles[fileName] = { postsFiles[fileName] = {
type: 'file', type: "file",
name: fileName, name: fileName,
content content,
}; };
}); });
@ -168,18 +200,17 @@ ${post.content}`;
function buildTalksFiles(): { [key: string]: FileSystemNode } { function buildTalksFiles(): { [key: string]: FileSystemNode } {
const talksFiles: { [key: string]: FileSystemNode } = {}; const talksFiles: { [key: string]: FileSystemNode } = {};
talks.forEach(talk => { talks.forEach((talk) => {
const fileName = `${talk.id}.txt`; const fileName = `${talk.id}.txt`;
let content = `${talk.name} let content = `${talk.name}
${talk.description} ${talk.description}
${talk.venue || ''} ${talk.date || ""}
${talk.date || ''}
${talk.link}`; ${talk.link}`;
talksFiles[fileName] = { talksFiles[fileName] = {
type: 'file', type: "file",
name: fileName, name: fileName,
content content,
}; };
}); });
@ -189,18 +220,18 @@ ${talk.link}`;
function buildProjectsFiles(): { [key: string]: FileSystemNode } { function buildProjectsFiles(): { [key: string]: FileSystemNode } {
const projectsFiles: { [key: string]: FileSystemNode } = {}; const projectsFiles: { [key: string]: FileSystemNode } = {};
projects.forEach(project => { projects.forEach((project) => {
const fileName = `${project.id}.txt`; const fileName = `${project.id}.txt`;
let content = `${project.name} let content = `${project.name}
${project.description} ${project.description}
${project.status || ''} ${project.status || ""}
${project.technologies ? project.technologies.join(', ') : ''} ${project.technologies ? project.technologies.join(", ") : ""}
${project.link}`; ${project.link}`;
projectsFiles[fileName] = { projectsFiles[fileName] = {
type: 'file', type: "file",
name: fileName, name: fileName,
content content,
}; };
}); });
@ -210,15 +241,15 @@ ${project.link}`;
function buildSocialFiles(): { [key: string]: FileSystemNode } { function buildSocialFiles(): { [key: string]: FileSystemNode } {
const socialFiles: { [key: string]: FileSystemNode } = {}; const socialFiles: { [key: string]: FileSystemNode } = {};
socialLinks.forEach(link => { socialLinks.forEach((link) => {
const fileName = `${link.id}.txt`; const fileName = `${link.id}.txt`;
let content = `${link.name} let content = `${link.name}
${link.url}`; ${link.url}`;
socialFiles[fileName] = { socialFiles[fileName] = {
type: 'file', type: "file",
name: fileName, name: fileName,
content content,
}; };
}); });
@ -228,76 +259,126 @@ ${link.url}`;
function buildTechFiles(): { [key: string]: FileSystemNode } { function buildTechFiles(): { [key: string]: FileSystemNode } {
const techFiles: { [key: string]: FileSystemNode } = {}; const techFiles: { [key: string]: FileSystemNode } = {};
techLinks.forEach(link => { techLinks.forEach((link) => {
const fileName = `${link.id}.txt`; const fileName = `${link.id}.txt`;
let content = `${link.name} let content = `${link.name}
${link.url}`; ${link.url}`;
techFiles[fileName] = { techFiles[fileName] = {
type: 'file', type: "file",
name: fileName, name: fileName,
content content,
}; };
}); });
return techFiles; return techFiles;
} }
function buildContactContent(resumeData: ResumeData): string { function buildContactContent(resumeData: any): string {
return [ try {
`Email: ${resumeData.basics.email}`, const basics = resumeData.basics || {};
'', const email = basics.email || "Not provided";
'Social Profiles:', const profiles = basics.profiles || [];
...resumeData.sections.profiles.items.map(profile =>
`${profile.network}: ${profile.url.href}` return [
) `Email: ${email}`,
].join('\n'); "",
"Social Profiles:",
...profiles.map((profile: any) => `${profile.network}: ${profile.url}`),
].join("\n");
} catch (error) {
console.error("Error building contact content:", error);
return "Contact information unavailable";
}
} }
function buildFallbackFileSystem(): { [key: string]: FileSystemNode } { function buildFallbackFileSystem(): { [key: string]: FileSystemNode } {
const talksFiles = buildTalksFiles();
const projectsFiles = buildProjectsFiles();
const socialFiles = buildSocialFiles();
const techFiles = buildTechFiles();
return { return {
'/': { "/": {
type: 'directory', type: "directory",
name: '/', name: "/",
children: { children: {
'about.txt': { "about.txt": {
type: 'file', type: "file",
name: 'about.txt', name: "about.txt",
content: 'Atridad Lahiji\nResearcher, Full-Stack Developer, and IT Professional.\n\nError loading detailed information. Please check the website directly.' content:
} "Atridad Lahiji\nResearcher, Full-Stack Developer, and IT Professional.\n\nError loading resume data. Basic navigation still available.\n\nExplore the directories:\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.",
} },
} 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,
},
"help.txt": {
type: "file",
name: "help.txt",
content:
"Available commands:\n- ls - list files\n- cd <directory> - change directory\n- cat <file> - view file contents\n- pwd - show current directory\n- clear - clear terminal\n- help - show this help\n- train - run the train animation",
},
},
},
}; };
} }
export function getCurrentDirectory(fileSystem: { [key: string]: FileSystemNode }, currentPath: string): FileSystemNode { export function getCurrentDirectory(
const pathParts = currentPath.split('/').filter((part: string) => part !== ''); fileSystem: { [key: string]: FileSystemNode },
let current = fileSystem['/']; currentPath: string,
): FileSystemNode {
const pathParts = currentPath
.split("/")
.filter((part: string) => part !== "");
let current = fileSystem["/"];
for (const part of pathParts) { for (const part of pathParts) {
if (current?.children && current.children[part] && current.children[part].type === 'directory') { if (
current?.children &&
current.children[part] &&
current.children[part].type === "directory"
) {
current = current.children[part]; current = current.children[part];
} }
} }
return current; return current;
} }
export function resolvePath(currentPath: string, path: string): string { export function resolvePath(currentPath: string, path: string): string {
if (path.startsWith('/')) { if (path.startsWith("/")) {
return path; return path;
} }
const currentParts = currentPath.split('/').filter((part: string) => part !== ''); const currentParts = currentPath
const pathParts = path.split('/'); .split("/")
.filter((part: string) => part !== "");
const pathParts = path.split("/");
for (const part of pathParts) { for (const part of pathParts) {
if (part === '..') { if (part === "..") {
currentParts.pop(); currentParts.pop();
} else if (part !== '.' && part !== '') { } else if (part !== "." && part !== "") {
currentParts.push(part); currentParts.push(part);
} }
} }
return '/' + currentParts.join('/'); return "/" + currentParts.join("/");
} }