2.0.0 - Overhaul
All checks were successful
Docker Deploy / build-and-push (push) Successful in 4m13s
All checks were successful
Docker Deploy / build-and-push (push) Successful in 4m13s
This commit is contained in:
@@ -20,18 +20,6 @@ url = "https://git.atri.dad/atridad"
|
||||
[summary]
|
||||
content = "I am a full-stack web developer and researcher with a background maintaining and developing for large-scale enterprise software systems."
|
||||
|
||||
[[experience]]
|
||||
company = "Atash Consulting"
|
||||
position = "Owner/Developer"
|
||||
location = "Edmonton, Alberta"
|
||||
date = "June 2019 – Present"
|
||||
description = [
|
||||
"Builds mobile and web applications for small-medium sized businesses",
|
||||
"Provides consulting on application development, system architecture, DevOps, etc",
|
||||
"Hosting websites for small-medium sized businesses",
|
||||
]
|
||||
url = "https://atash.dev"
|
||||
|
||||
[[experience]]
|
||||
company = "University of Saskatchewan CEPHIL Lab"
|
||||
position = "Technical Lead"
|
||||
@@ -42,9 +30,24 @@ description = [
|
||||
"Developing mobile and web applications with Flutter and React (Astro)",
|
||||
"Coordinating with other grant researchers to deliver a minimum viable product",
|
||||
"Gathering requirements from stakeholders to craft a product timeline",
|
||||
"Making architectural decisions for system-wide components (database, deployment, applications, web dashboard, etc.)",
|
||||
"Contributing to the web-based dashboard development",
|
||||
]
|
||||
url = "https://cephil.ca/"
|
||||
|
||||
[[experience]]
|
||||
company = "Atash Consulting"
|
||||
position = "Owner/Developer"
|
||||
location = "Edmonton, Alberta"
|
||||
date = "June 2019 – Present"
|
||||
description = [
|
||||
"Building native mobile applications and their accompanying APIs for small-to-medium sized businesses",
|
||||
"Building web applications for small-to-medium sized businesses",
|
||||
"Providing consulting on application development, system architecture, DevOps, etc.",
|
||||
"Hosting websites for small-to-medium sized businesses",
|
||||
]
|
||||
url = "https://atash.dev"
|
||||
|
||||
[[experience]]
|
||||
company = "Alberta Motor Association"
|
||||
position = "Software Developer II"
|
||||
|
||||
@@ -10,20 +10,20 @@ const { title, description: blurb, pubDate } = post.data;
|
||||
const { slug } = post;
|
||||
---
|
||||
|
||||
<div class="card bg-accent w-full max-w-sm shrink shadow-md">
|
||||
<div class="card bg-accent text-accent-content w-full max-w-sm shrink shadow-md">
|
||||
<div class="card-body break-words">
|
||||
<h2
|
||||
class="card-title text-xl md:text-2xl font-bold justify-center text-center break-words text-base-100"
|
||||
class="card-title text-xl md:text-2xl font-bold justify-center text-center break-words"
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
|
||||
<p class="text-center break-words my-4 text-base-100">
|
||||
<p class="text-center break-words my-4">
|
||||
{blurb || "No description available."}
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="flex flex-wrap items-center justify-center text-base-100 opacity-75 gap-2 text-sm mb-4"
|
||||
class="flex flex-wrap items-center justify-center opacity-75 gap-2 text-sm mb-4"
|
||||
>
|
||||
<Icon name="mdi:clock" class="text-xl" />
|
||||
<span>
|
||||
@@ -41,7 +41,7 @@ const { slug } = post;
|
||||
post.data.tags && post.data.tags.length > 0 && (
|
||||
<div class="flex gap-2 flex-wrap mb-4 justify-center">
|
||||
{post.data.tags.map((tag: string) => (
|
||||
<div class="badge badge-primary font-bold text-base-100">
|
||||
<div class="badge badge-primary font-bold">
|
||||
<Icon name="mdi:tag" class="text-lg" />
|
||||
{tag}
|
||||
</div>
|
||||
@@ -53,7 +53,7 @@ const { slug } = post;
|
||||
<div class="card-actions justify-end">
|
||||
<a
|
||||
href={`/post/${slug}`}
|
||||
class="btn btn-circle text-accent"
|
||||
class="btn btn-circle"
|
||||
aria-label={`Read more about ${title}`}
|
||||
>
|
||||
<Icon name="mdi:arrow-right" class="text-lg" />
|
||||
|
||||
@@ -9,15 +9,15 @@ interface Props {
|
||||
const { project } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="card bg-accent w-full max-w-sm shrink shadow-md">
|
||||
<div class="card bg-accent text-accent-content w-full max-w-sm shrink shadow-md">
|
||||
<div class="card-body break-words">
|
||||
<h2
|
||||
class="card-title text-xl md:text-2xl font-bold justify-center text-center break-words text-base-100"
|
||||
class="card-title text-xl md:text-2xl font-bold justify-center text-center break-words"
|
||||
>
|
||||
{project.name}
|
||||
</h2>
|
||||
|
||||
<p class="text-center break-words text-base-100">
|
||||
<p class="text-center break-words">
|
||||
{project.description}
|
||||
</p>
|
||||
|
||||
@@ -26,7 +26,7 @@ const { project } = Astro.props;
|
||||
href={project.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn btn-circle text-accent"
|
||||
class="btn btn-circle"
|
||||
aria-label={`Visit ${project.name}`}
|
||||
>
|
||||
<Icon name="mdi:link" class="text-lg" />
|
||||
|
||||
@@ -52,7 +52,7 @@ export default function ResumeDownloadButton({
|
||||
class={`btn bg-primary font-bold rounded-full inline-flex items-center gap-2 text-sm sm:text-base ${
|
||||
isLoading
|
||||
? "text-primary border-2 border-primary"
|
||||
: "text-base-100"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{isLoading ? (
|
||||
|
||||
@@ -162,7 +162,7 @@ export default function ResumeSettingsModal({
|
||||
{/* Floating Settings Button */}
|
||||
<button
|
||||
onClick={openModal}
|
||||
class={`fixed top-4 right-4 z-20 bg-secondary text-base-100 hover:bg-primary btn btn-circle ${className}`}
|
||||
class={`fixed top-4 right-4 z-20 bg-secondary hover:bg-primary btn btn-circle ${className}`}
|
||||
aria-label="Resume Settings"
|
||||
>
|
||||
<Settings class="text-lg" />
|
||||
@@ -175,7 +175,7 @@ export default function ResumeSettingsModal({
|
||||
<h3 class="font-bold text-lg">Resume Generator</h3>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
class="btn btn-circle bg-secondary hover:bg-primary text-base-100"
|
||||
class="btn btn-circle bg-secondary hover:bg-primary"
|
||||
>
|
||||
<X className="text-lg" />
|
||||
</button>
|
||||
@@ -189,10 +189,10 @@ export default function ResumeSettingsModal({
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div class="flex flex-wrap gap-2 mb-6">
|
||||
<button onClick={downloadTemplate} class="btn bg-primary btn-sm font-bold text-base-100">
|
||||
<button onClick={downloadTemplate} class="btn bg-primary btn-sm font-bold">
|
||||
Download Template
|
||||
</button>
|
||||
<button onClick={loadTemplate} class="btn bg-secondary btn-sm font-bold text-base-100">
|
||||
<button onClick={loadTemplate} class="btn bg-secondary btn-sm font-bold">
|
||||
Load Template in Editor
|
||||
</button>
|
||||
</div>
|
||||
@@ -207,7 +207,7 @@ export default function ResumeSettingsModal({
|
||||
role="tab"
|
||||
class={`px-4 py-2 rounded-full text-sm transition-all duration-200 font-bold ${
|
||||
activeTab === "upload"
|
||||
? "bg-primary shadow-sm text-base-100"
|
||||
? "bg-primary shadow-sm"
|
||||
: "text-base-content/70 hover:text-base-content hover:bg-base-200"
|
||||
}`}
|
||||
onClick={() => setActiveTab("upload")}
|
||||
@@ -218,7 +218,7 @@ export default function ResumeSettingsModal({
|
||||
role="tab"
|
||||
class={`px-4 py-2 rounded-full text-sm font-bold transition-all duration-200 ${
|
||||
activeTab === "edit"
|
||||
? "bg-primary font-bold text-base-100 shadow-sm"
|
||||
? "bg-primary font-bold shadow-sm"
|
||||
: "text-base-content/70 hover:text-base-content font-bold hover:bg-base-200"
|
||||
}`}
|
||||
onClick={() => setActiveTab("edit")}
|
||||
|
||||
@@ -29,7 +29,7 @@ export default function ScrollUpButton() {
|
||||
return (
|
||||
<button
|
||||
onClick={scrollToTop}
|
||||
class={`fixed bottom-4 right-4 z-20 bg-secondary hover:bg-primary text-base-100
|
||||
class={`fixed bottom-4 right-4 z-20 bg-secondary hover:bg-primary
|
||||
btn btn-circle transition-all duration-300
|
||||
${
|
||||
isVisible.value
|
||||
|
||||
@@ -10,16 +10,16 @@ const { talk } = Astro.props;
|
||||
---
|
||||
|
||||
<div
|
||||
class="card bg-accent shadow-lg w-full sm:w-[calc(50%-1rem)] md:w-96 min-w-[280px] max-w-sm shrink"
|
||||
class="card bg-accent text-accent-content shadow-lg w-full sm:w-[calc(50%-1rem)] md:w-96 min-w-[280px] max-w-sm shrink"
|
||||
>
|
||||
<div class="card-body p-6 break-words">
|
||||
<h2
|
||||
class="card-title text-xl md:text-2xl font-bold justify-center text-center break-words text-base-100"
|
||||
class="card-title text-xl md:text-2xl font-bold justify-center text-center break-words"
|
||||
>
|
||||
{talk.name}
|
||||
</h2>
|
||||
|
||||
<p class="text-center break-words my-4 text-base-100">
|
||||
<p class="text-center break-words my-4">
|
||||
{talk.description}
|
||||
</p>
|
||||
|
||||
@@ -39,7 +39,7 @@ const { talk } = Astro.props;
|
||||
href={talk.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn btn-circle text-accent"
|
||||
class="btn btn-circle"
|
||||
aria-label={`Visit ${talk.name}`}
|
||||
>
|
||||
<Icon name="mdi:link" class="text-lg" />
|
||||
|
||||
@@ -1,234 +0,0 @@
|
||||
import { useState, useEffect, useRef } from "preact/hooks";
|
||||
import type { JSX } from "preact";
|
||||
import type { Command } from "../utils/terminal/types";
|
||||
import { buildFileSystem } from "../utils/terminal/fs";
|
||||
import {
|
||||
executeCommand,
|
||||
type CommandContext,
|
||||
} from "../utils/terminal/commands";
|
||||
import {
|
||||
getCompletions,
|
||||
formatOutput,
|
||||
saveCommandToHistory,
|
||||
loadCommandHistory,
|
||||
} from "../utils/terminal/utils";
|
||||
|
||||
const Terminal = () => {
|
||||
const [currentPath, setCurrentPath] = useState("/");
|
||||
const [commandHistory, setCommandHistory] = useState<Command[]>([
|
||||
{
|
||||
input: "",
|
||||
output:
|
||||
'Welcome to Atridad\'s Shell!\nType "help" to see available commands.\n',
|
||||
timestamp: new Date(),
|
||||
path: "/",
|
||||
},
|
||||
]);
|
||||
const [currentInput, setCurrentInput] = useState("");
|
||||
const [historyIndex, setHistoryIndex] = useState(-1);
|
||||
const [fileSystem, setFileSystem] = useState<{ [key: string]: any }>({});
|
||||
const [isTrainRunning, setIsTrainRunning] = useState(false);
|
||||
const [trainPosition, setTrainPosition] = useState(100);
|
||||
const [persistentHistory, setPersistentHistory] = useState<string[]>([]);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const terminalRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (terminalRef.current && !isTrainRunning) {
|
||||
terminalRef.current.scrollTop = terminalRef.current.scrollHeight;
|
||||
}
|
||||
}, [commandHistory, isTrainRunning]);
|
||||
|
||||
// Load command history from localStorage
|
||||
useEffect(() => {
|
||||
const history = loadCommandHistory();
|
||||
setPersistentHistory(history);
|
||||
}, []);
|
||||
|
||||
// Initialize file system
|
||||
useEffect(() => {
|
||||
buildFileSystem().then(setFileSystem);
|
||||
}, []);
|
||||
|
||||
const handleSubmit = (e: JSX.TargetedEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
const commandContext: CommandContext = {
|
||||
currentPath,
|
||||
fileSystem,
|
||||
setCurrentPath,
|
||||
setIsTrainRunning,
|
||||
setTrainPosition,
|
||||
};
|
||||
|
||||
const output = executeCommand(currentInput, commandContext);
|
||||
const newCommand: Command = {
|
||||
input: currentInput,
|
||||
output,
|
||||
timestamp: new Date(),
|
||||
path: currentPath,
|
||||
};
|
||||
|
||||
// Save command to persistent history
|
||||
const updatedHistory = saveCommandToHistory(
|
||||
currentInput,
|
||||
persistentHistory,
|
||||
);
|
||||
setPersistentHistory(updatedHistory);
|
||||
|
||||
if (currentInput.trim().toLowerCase() === "clear") {
|
||||
setCommandHistory([]);
|
||||
} else {
|
||||
setCommandHistory((prev: Command[]) => [...prev, newCommand]);
|
||||
}
|
||||
|
||||
setCurrentInput("");
|
||||
setHistoryIndex(-1);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: JSX.TargetedKeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Tab") {
|
||||
e.preventDefault();
|
||||
|
||||
const { completion, replaceFrom } = getCompletions(
|
||||
currentInput,
|
||||
currentPath,
|
||||
fileSystem,
|
||||
);
|
||||
|
||||
if (completion) {
|
||||
const beforeReplacement = currentInput.substring(0, replaceFrom);
|
||||
const newInput = beforeReplacement + completion;
|
||||
setCurrentInput(newInput + (completion.endsWith("/") ? "" : " "));
|
||||
}
|
||||
} else if (e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
if (persistentHistory.length > 0) {
|
||||
const newIndex =
|
||||
historyIndex === -1
|
||||
? persistentHistory.length - 1
|
||||
: Math.max(0, historyIndex - 1);
|
||||
setHistoryIndex(newIndex);
|
||||
setCurrentInput(persistentHistory[newIndex]);
|
||||
}
|
||||
} 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
|
||||
) {
|
||||
setHistoryIndex(-1);
|
||||
setCurrentInput("");
|
||||
} else {
|
||||
setHistoryIndex(newIndex);
|
||||
setCurrentInput(persistentHistory[newIndex]);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-base-100 text-base-content font-mono text-sm h-full flex flex-col rounded-lg border-2 border-primary shadow-2xl relative">
|
||||
<div className="bg-base-200 px-4 py-2 rounded-t-lg border-b border-base-300">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-3 h-3 bg-error rounded-full"></div>
|
||||
<div className="w-3 h-3 bg-warning rounded-full"></div>
|
||||
<div className="w-3 h-3 bg-success rounded-full"></div>
|
||||
<span className="ml-4 text-base-content/70 text-xs">
|
||||
guest@atri.dad: {currentPath}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={terminalRef}
|
||||
className={`flex-1 p-4 overflow-y-auto scrollbar-thin scrollbar-thumb-base-300 scrollbar-track-base-100 relative ${
|
||||
isTrainRunning ? "opacity-0" : "opacity-100"
|
||||
}`}
|
||||
onClick={() => !isTrainRunning && inputRef.current?.focus()}
|
||||
>
|
||||
<div className="min-h-full">
|
||||
{commandHistory.map((command: Command, index: number) => (
|
||||
<div key={index} className="mb-2">
|
||||
{command.input && (
|
||||
<div className="flex items-center">
|
||||
<span className="text-primary font-semibold">
|
||||
guest@atri.dad
|
||||
</span>
|
||||
<span className="text-base-content">:</span>
|
||||
<span className="text-secondary font-semibold">
|
||||
{command.path}
|
||||
</span>
|
||||
<span className="text-base-content">$ </span>
|
||||
<span className="text-accent">{command.input}</span>
|
||||
</div>
|
||||
)}
|
||||
{command.output && (
|
||||
<div
|
||||
className="whitespace-pre-wrap text-base-content/80 mt-1"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: formatOutput(command.output),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{!isTrainRunning && (
|
||||
<form onSubmit={handleSubmit} className="flex items-center">
|
||||
<span className="text-primary font-semibold">guest@atri.dad</span>
|
||||
<span className="text-base-content">:</span>
|
||||
<span className="text-secondary font-semibold">
|
||||
{currentPath}
|
||||
</span>
|
||||
<span className="text-base-content">$ </span>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={currentInput}
|
||||
onInput={(e) =>
|
||||
setCurrentInput((e.target as HTMLInputElement).value)
|
||||
}
|
||||
onKeyDown={handleKeyDown}
|
||||
className="flex-1 bg-transparent border-none outline-none text-accent ml-1"
|
||||
spellcheck={false}
|
||||
/>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Train animation overlay - positioned over the content area but outside the opacity div */}
|
||||
{isTrainRunning && (
|
||||
<div className="absolute inset-x-0 top-16 bottom-0 flex items-center justify-center overflow-hidden pointer-events-none">
|
||||
<div
|
||||
className="text-white font-mono text-xs whitespace-nowrap"
|
||||
style={{
|
||||
transform: `translateX(${trainPosition}%)`,
|
||||
transition: "none",
|
||||
}}
|
||||
>
|
||||
<pre className="leading-none">{`
|
||||
==== ________ ___________
|
||||
_D _| |_______/ \\__I_I_____===__|_________|
|
||||
|(_)--- | H\\________/ | | =|___ ___| _________________
|
||||
/ | | H | | | | ||_| |_|| _| \\_____A
|
||||
| | | H |__--------------------| [___] | =| |
|
||||
| ________|___H__/__|_____/[][]~\\_______| | -| |
|
||||
|/ | |-----------I_____I [][] [] D |=======|____|________________________|_
|
||||
__/ =| o |=-O=====O=====O=====O \\ ____Y___________|__|__________________________|_
|
||||
|/-=|___|= || || || |_____/~\\___/ |_D__D__D_| |_D__D__D_|
|
||||
\\_/ \\__/ \\__/ \\__/ \\__/ \\_/ \\_/ \\_/ \\_/ \\_/`}</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Terminal;
|
||||
@@ -57,7 +57,7 @@ export const homepageSections: HomepageSections = {
|
||||
description: "Find me across the web",
|
||||
},
|
||||
techStack: {
|
||||
title: "Stuff I Use:",
|
||||
title: "Technologies I Use:",
|
||||
description: "Technologies and tools I work with",
|
||||
},
|
||||
};
|
||||
@@ -131,6 +131,12 @@ export const talks: Talk[] = [
|
||||
];
|
||||
|
||||
export const projects: Project[] = [
|
||||
{
|
||||
id: "openclimb",
|
||||
name: "OpenClimb",
|
||||
description: "Jeckpack Compose based Rock Climbing Tracker",
|
||||
link: "https://git.atri.dad/atridad/OpenClimb"
|
||||
},
|
||||
{
|
||||
id: "mealient",
|
||||
name: "Mealient (Fork of project by Kirill Kamakin)",
|
||||
@@ -340,12 +346,4 @@ export const navigationItems: NavigationItem[] = [
|
||||
enabled: true,
|
||||
isActive: (path: string) => path.startsWith("/talks"),
|
||||
},
|
||||
{
|
||||
id: "terminal",
|
||||
name: "Terminal",
|
||||
path: "/terminal",
|
||||
tooltip: "Terminal",
|
||||
icon: TerminalIcon,
|
||||
enabled: true,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
title: "'Responsible AI Use'"
|
||||
description: "A reflection on AI slop"
|
||||
pubDate: "2025-08-12"
|
||||
tags: ["ai", "opinion"]
|
||||
---
|
||||
|
||||
# Tl;dr
|
||||
|
||||
There is no such thing as responsible AI use. None. Zero. That's it. If you want more nuance, read on.
|
||||
|
||||
# Houston, we have a problem
|
||||
|
||||
Generative AI is cancer that grows ever stronger, and I've just about had enough. From people who have 0 creative abilities claiming the 15-fingered monstrosities they generate constitute "art" to the "developers" getting paid to vibe code, it's all pure insanity. No actual work was put into these creations. No real thought. No creativity. No soul. Just slightly more advanced autocomplete engine spitting out soul-less garbage we seem to collectively celebrate.
|
||||
|
||||
What confuses me about this whole thing is how the people using this have allowed their skills to atrophy, specifically developers. Any skill needs repetition to keep current and to maintain, and the same is true for programming. Vibe coding is an excellent way to prevent you from doing the things that actually grow your skillset: trial and error, reading docs, parsing stack traces, using a debugger, etc.
|
||||
|
||||
# Copywrong
|
||||
|
||||
And then there is, of course, the issue of intellectual property. We are all aware of the obvious ethical implications of a machine learning model trained on billions of copyrighted works being used to generate "new" without attribution or compensation to the original creators. But really, the entire legal framework around copyright, fair use, and ownership is imploding.
|
||||
|
||||
Artists, writers, musicians are actively having their livelihoods undermined by algorithms that take their work and compete against them. The very concept of originality is being eroded. Why would someone, other than being a generally good person, commission a painter when a "prompt engineer" can generate 100 variations in an hour? Why pay a writer when ChatGPT can generate blog posts in an instant? Human creation is losing its value.
|
||||
|
||||
# The Desecration of Creation
|
||||
|
||||
Proponents of GenAI will go on and on about "democratizing creativity" or "lowering the barrier to entry." All it's doing is flooding the market with slop, making it even harder for people who put hard work into developing their crafts to stand out. Ultimately, it's not about empowering more people to create; it's about empowering people to pretend they've created something, without any time, effort, and passion that real creation demands.
|
||||
|
||||
What we're doing is fostering a culture of instant gratification and superficiality. Why bother learning to draw when you can prompt MidJourney? Why spend years perfecting your craft when a model can spit out something "good enough"? This is not progress; it's intellectual laziness on a massive scale. We are actively choosing to outsource our most human qualities to machines, which is just outright shameful.
|
||||
|
||||
So, no. There is no responsible AI use. Not when its very foundation is built on the stolen work of skilled individuals and definitely not when it devalues the very essence of human creativity.
|
||||
@@ -19,7 +19,7 @@ import { personalInfo, homepageSections } from "../config/data";
|
||||
/>
|
||||
|
||||
<h1
|
||||
class="bg-gradient-to-r from-primary via-secondary to-accent bg-clip-text text-transparent text-4xl sm:text-6xl font-bold text-center"
|
||||
class="text-primary text-4xl sm:text-6xl font-bold text-center"
|
||||
>
|
||||
{personalInfo.name}
|
||||
</h1>
|
||||
|
||||
@@ -45,7 +45,7 @@ const { Content } = await post.render();
|
||||
</div>
|
||||
|
||||
{/* Back button */}
|
||||
<a href="/posts" class="btn btn-outline btn-primary btn-sm font-bold hover:text-base-100">
|
||||
<a href="/posts" class="btn btn-outline btn-primary btn-sm font-bold">
|
||||
<Icon name="mdi:arrow-left" class="text-lg" />
|
||||
Back
|
||||
</a>
|
||||
@@ -55,7 +55,7 @@ const { Content } = await post.render();
|
||||
post.data.tags && post.data.tags.length > 0 && (
|
||||
<div class="flex gap-2 flex-wrap mb-6">
|
||||
{post.data.tags.map((tag: string) => (
|
||||
<div class="badge badge-primary font-bold text-base-100">
|
||||
<div class="badge badge-primary font-bold">
|
||||
<Icon name="mdi:tag" class="text-lg" />
|
||||
{tag}
|
||||
</div>
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
---
|
||||
import Layout from "../layouts/Layout.astro";
|
||||
import TerminalComponent from "../components/Terminal";
|
||||
import "../styles/global.css";
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<div class="container mx-auto p-4 max-w-6xl w-full">
|
||||
<div class="mb-4 text-center">
|
||||
<h1
|
||||
class="text-3xl sm:text-4xl font-bold text-primary mb-6 sm:mb-8 text-center"
|
||||
>
|
||||
Terminal
|
||||
</h1>
|
||||
</div>
|
||||
<div class="h-[60vh] max-h-[500px] min-h-[400px]">
|
||||
<TerminalComponent client:load />
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
height: auto;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -3,35 +3,36 @@
|
||||
@plugin "@tailwindcss/typography";
|
||||
|
||||
@plugin "daisyui/theme" {
|
||||
name: "chaoticbisexual";
|
||||
default: true;
|
||||
prefersdark: true;
|
||||
color-scheme: "dark";
|
||||
--color-base-100: oklch(25.33% 0.016 252.42);
|
||||
--color-base-200: oklch(23.26% 0.014 253.1);
|
||||
--color-base-300: oklch(21.15% 0.012 254.09);
|
||||
--color-base-content: oklch(97.807% 0.029 256.847);
|
||||
--color-primary: oklch(65% 0.241 354.308);
|
||||
--color-primary-content: oklch(96% 0.018 272.314);
|
||||
--color-secondary: oklch(60% 0.25 292.717);
|
||||
--color-secondary-content: oklch(94% 0.028 342.258);
|
||||
--color-accent: oklch(78% 0.154 211.53);
|
||||
--color-accent-content: oklch(38% 0.063 188.416);
|
||||
--color-neutral: oklch(40% 0.17 325.612);
|
||||
--color-neutral-content: oklch(92% 0.004 286.32);
|
||||
--color-info: oklch(74% 0.16 232.661);
|
||||
--color-info-content: oklch(29% 0.066 243.157);
|
||||
--color-success: oklch(76% 0.177 163.223);
|
||||
--color-success-content: oklch(37% 0.077 168.94);
|
||||
--color-warning: oklch(82% 0.189 84.429);
|
||||
--color-warning-content: oklch(41% 0.112 45.904);
|
||||
--color-error: oklch(71% 0.194 13.428);
|
||||
--color-error-content: oklch(27% 0.105 12.094);
|
||||
--radius-selector: 2rem;
|
||||
--radius-field: 2rem;
|
||||
--radius-box: 2rem;
|
||||
--size-selector: 0.25rem;
|
||||
--size-field: 0.25rem;
|
||||
--border: 1.5px;
|
||||
--depth: 1;
|
||||
}
|
||||
name: "atridotdad";
|
||||
default: true;
|
||||
prefersdark: true;
|
||||
color-scheme: "dark";
|
||||
--color-base-100: oklch(25.33% 0.016 252.42);
|
||||
--color-base-200: oklch(23.26% 0.014 253.1);
|
||||
--color-base-300: oklch(21.15% 0.012 254.09);
|
||||
--color-base-content: oklch(100% 0 0);
|
||||
--color-primary: oklch(78% 0.154 211.53);
|
||||
--color-primary-content: oklch(0% 0 0);
|
||||
--color-secondary: oklch(68% 0.169 237.323);
|
||||
--color-secondary-content: oklch(0% 0 0);
|
||||
--color-accent: oklch(60% 0.126 221.723);
|
||||
--color-accent-content: oklch(0% 0 0);
|
||||
--color-neutral: oklch(45% 0.085 224.283);
|
||||
--color-neutral-content: oklch(100% 0 0);
|
||||
--color-info: oklch(74% 0.16 232.661);
|
||||
--color-info-content: oklch(29% 0.066 243.157);
|
||||
--color-success: oklch(70% 0.14 182.503);
|
||||
--color-success-content: oklch(37% 0.077 168.94);
|
||||
--color-warning: oklch(82% 0.189 84.429);
|
||||
--color-warning-content: oklch(41% 0.112 45.904);
|
||||
--color-error: oklch(71% 0.194 13.428);
|
||||
--color-error-content: oklch(27% 0.105 12.094);
|
||||
--radius-selector: 2rem;
|
||||
--radius-field: 2rem;
|
||||
--radius-box: 2rem;
|
||||
--size-selector: 0.25rem;
|
||||
--size-field: 0.25rem;
|
||||
--border: 1.5px;
|
||||
--depth: 1;
|
||||
--noise: 0;
|
||||
}
|
||||
|
||||
@@ -1,250 +0,0 @@
|
||||
import type { FileSystemNode } from "./types";
|
||||
import { getCurrentDirectory, resolvePath } from "./fs";
|
||||
|
||||
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 <path> - Change directory
|
||||
cat <file> - Display file contents
|
||||
pwd - Show current directory
|
||||
clear - Clear terminal
|
||||
tree - Show directory structure
|
||||
whoami - Display user info
|
||||
open <path> - 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 "";
|
||||
}
|
||||
@@ -1,384 +0,0 @@
|
||||
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("/api/resume.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
|
||||
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: any): { [key: string]: FileSystemNode } {
|
||||
const resumeFiles: { [key: string]: FileSystemNode } = {};
|
||||
|
||||
try {
|
||||
if (resumeData.summary) {
|
||||
resumeFiles["summary.txt"] = {
|
||||
type: "file",
|
||||
name: "summary.txt",
|
||||
content: resumeData.summary.content,
|
||||
};
|
||||
}
|
||||
|
||||
if (resumeData.skills && Array.isArray(resumeData.skills)) {
|
||||
const skillsContent = resumeData.skills
|
||||
.map((skill: any) => `${skill.name} (Level: ${skill.level}/5)`)
|
||||
.join("\n");
|
||||
resumeFiles["skills.txt"] = {
|
||||
type: "file",
|
||||
name: "skills.txt",
|
||||
content: skillsContent,
|
||||
};
|
||||
}
|
||||
|
||||
if (resumeData.experience && Array.isArray(resumeData.experience)) {
|
||||
const experienceContent = resumeData.experience
|
||||
.map((exp: any) => {
|
||||
const description = Array.isArray(exp.description)
|
||||
? exp.description.join("\n• ")
|
||||
: "";
|
||||
return `${exp.position} at ${exp.company}\n${exp.date} | ${exp.location}\n• ${description}\n${exp.url ? `URL: ${exp.url}` : ""}\n`;
|
||||
})
|
||||
.join("\n---\n\n");
|
||||
resumeFiles["experience.txt"] = {
|
||||
type: "file",
|
||||
name: "experience.txt",
|
||||
content: experienceContent,
|
||||
};
|
||||
}
|
||||
|
||||
if (resumeData.education && Array.isArray(resumeData.education)) {
|
||||
const educationContent = resumeData.education
|
||||
.map(
|
||||
(edu: any) =>
|
||||
`${edu.institution}\n${edu.degree} - ${edu.field}\n${edu.date}\n${edu.details && Array.isArray(edu.details) ? edu.details.join("\n• ") : ""}`,
|
||||
)
|
||||
.join("\n\n---\n\n");
|
||||
resumeFiles["education.txt"] = {
|
||||
type: "file",
|
||||
name: "education.txt",
|
||||
content: educationContent,
|
||||
};
|
||||
}
|
||||
|
||||
if (resumeData.volunteer && Array.isArray(resumeData.volunteer)) {
|
||||
const volunteerContent = resumeData.volunteer
|
||||
.map((vol: any) => `${vol.organization}\n${vol.position}\n${vol.date}`)
|
||||
.join("\n\n---\n\n");
|
||||
resumeFiles["volunteer.txt"] = {
|
||||
type: "file",
|
||||
name: "volunteer.txt",
|
||||
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;
|
||||
}
|
||||
|
||||
function buildPostsFiles(postsData: any[]): { [key: string]: FileSystemNode } {
|
||||
const postsFiles: { [key: string]: FileSystemNode } = {};
|
||||
|
||||
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 } = {};
|
||||
|
||||
talks.forEach((talk) => {
|
||||
const fileName = `${talk.id}.txt`;
|
||||
let content = `${talk.name}
|
||||
${talk.description}
|
||||
${talk.date || ""}
|
||||
${talk.link}`;
|
||||
|
||||
talksFiles[fileName] = {
|
||||
type: "file",
|
||||
name: fileName,
|
||||
content,
|
||||
};
|
||||
});
|
||||
|
||||
return talksFiles;
|
||||
}
|
||||
|
||||
function buildProjectsFiles(): { [key: string]: FileSystemNode } {
|
||||
const projectsFiles: { [key: string]: FileSystemNode } = {};
|
||||
|
||||
projects.forEach((project) => {
|
||||
const fileName = `${project.id}.txt`;
|
||||
let content = `${project.name}
|
||||
${project.description}
|
||||
${project.status || ""}
|
||||
${project.technologies ? project.technologies.join(", ") : ""}
|
||||
${project.link}`;
|
||||
|
||||
projectsFiles[fileName] = {
|
||||
type: "file",
|
||||
name: fileName,
|
||||
content,
|
||||
};
|
||||
});
|
||||
|
||||
return projectsFiles;
|
||||
}
|
||||
|
||||
function buildSocialFiles(): { [key: string]: FileSystemNode } {
|
||||
const socialFiles: { [key: string]: FileSystemNode } = {};
|
||||
|
||||
socialLinks.forEach((link) => {
|
||||
const fileName = `${link.id}.txt`;
|
||||
let content = `${link.name}
|
||||
${link.url}`;
|
||||
|
||||
socialFiles[fileName] = {
|
||||
type: "file",
|
||||
name: fileName,
|
||||
content,
|
||||
};
|
||||
});
|
||||
|
||||
return socialFiles;
|
||||
}
|
||||
|
||||
function buildTechFiles(): { [key: string]: FileSystemNode } {
|
||||
const techFiles: { [key: string]: FileSystemNode } = {};
|
||||
|
||||
techLinks.forEach((link) => {
|
||||
const fileName = `${link.id}.txt`;
|
||||
let content = `${link.name}
|
||||
${link.url}`;
|
||||
|
||||
techFiles[fileName] = {
|
||||
type: "file",
|
||||
name: fileName,
|
||||
content,
|
||||
};
|
||||
});
|
||||
|
||||
return techFiles;
|
||||
}
|
||||
|
||||
function buildContactContent(resumeData: any): string {
|
||||
try {
|
||||
const basics = resumeData.basics || {};
|
||||
const email = basics.email || "Not provided";
|
||||
const profiles = basics.profiles || [];
|
||||
|
||||
return [
|
||||
`Email: ${email}`,
|
||||
"",
|
||||
"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 } {
|
||||
const talksFiles = buildTalksFiles();
|
||||
const projectsFiles = buildProjectsFiles();
|
||||
const socialFiles = buildSocialFiles();
|
||||
const techFiles = buildTechFiles();
|
||||
|
||||
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 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 {
|
||||
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("/");
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
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[];
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
import type { FileSystemNode } from "./types";
|
||||
import { resolvePath } from "./fs";
|
||||
|
||||
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, '<span class="text-primary">')
|
||||
.replace(/\x1b\[0m/g, "</span>");
|
||||
}
|
||||
|
||||
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 [];
|
||||
}
|
||||
Reference in New Issue
Block a user