This commit is contained in:
164
README.md
164
README.md
@ -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
|
||||||
|
@ -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",
|
||||||
|
37
src/pages/api/resume.json.ts
Normal file
37
src/pages/api/resume.json.ts
Normal 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 });
|
||||||
|
}
|
||||||
|
};
|
@ -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);
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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(/ /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("/");
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user