Documentation Index
Fetch the complete documentation index at: https://mintlify.com/apursley2012/guest-portfolio.dev/llms.txt
Use this file to discover all available pages before exploring further.
Overview
Every command that returns visible content mounts a self-contained output component. These components live in components/outputs/ and are imported and rendered by Terminal.js. They are pure presentational components — they receive minimal props and manage any internal animation state themselves.
All output components receive no required props with two exceptions:
ManOutput requires a page string (the manual page name), and MailOutput
accepts an optional subject string (defaults to "Hello" if omitted).
Every other output component (AboutOutput, SkillsOutput, GitLogOutput,
AsciiBanner, MatrixRain) takes no props at all.
AsciiBanner
Triggered by: automatic render on terminal init (when history is empty)
AsciiBanner is the first thing a visitor sees. It displays the large DEV PORTFOLIO ASCII logotype followed by a welcome message typed out with TypewriterText (using fast={true} for a snappy speed).
// ASCII art rendered in a <pre> — hidden on mobile, visible sm+
<pre className="text-terminal-green font-bold text-xs sm:text-sm md:text-base leading-tight hidden sm:block">
{asciiArt} {/* multi-line DEV PORTFOLIO banner */}
</pre>
{/* Mobile fallback */}
<div className="sm:hidden text-terminal-green font-bold text-xl mb-4 border-b border-terminal-green pb-2">
DEV PORTFOLIO
</div>
<TypewriterText
text="Welcome to my personal server. Type 'help' to see available commands."
fast={true}
/>
The large banner is suppressed on small screens (hidden sm:block) and replaced with a plain text heading so the layout doesn’t break on narrow viewports.
AboutOutput
Triggered by: whoami or cat about.md
Renders a neofetch-style two-column layout: ASCII hackerman art on the left (desktop only), a key-value system info grid on the right, and a short bio paragraph below.
ASCII Art (left column)
const ascii = `
.----.
_.'__ '.
.--($)($$)---/#\\
.' @ /###\\
: , #####
'-..__.-' _.-\\###/
\`;_: \`"'
.'"""""'.
/, ya ,\\\\
// hack! \\\\
\`-._______.-'
___\\_\\__//___
/___/____\\___\\
`;
Hidden on mobile (hidden md:block), visible md+.
System Info Grid (right column)
<div className="grid grid-cols-[120px_1fr] gap-2 text-terminal-white">
<span className="text-terminal-green font-bold">OS:</span>
<span>Arch Linux x86_64 (btw)</span>
<span className="text-terminal-green font-bold">Host:</span>
<span>Human Body v1.0</span>
<span className="text-terminal-green font-bold">Uptime:</span>
<span>28 years, 4 months, 12 days</span>
<span className="text-terminal-green font-bold">Packages:</span>
<span>1337 (npm), 42 (pip)</span>
<span className="text-terminal-green font-bold">Shell:</span>
<span>zsh 5.8</span>
<span className="text-terminal-green font-bold">Editor:</span>
<span>nvim</span>
<span className="text-terminal-green font-bold">Languages:</span>
<span>TypeScript, Python, Rust, Go</span>
<span className="text-terminal-green font-bold">Coffee:</span>
<span>4 cups/day</span>
<span className="text-terminal-green font-bold">Sleep:</span>
<span className="text-terminal-red">Error: process not found</span>
</div>
Keys are green, values are white, the Sleep value is rendered in red for comedic effect.
Bio Paragraph
<div className="mt-6 text-terminal-white max-w-2xl">
<p className="mb-4">
Full-stack developer specializing in building exceptional digital
experiences. Currently focused on building accessible, human-centered
products at scale.
</p>
<p>
When I'm not at the computer, I'm usually reading sci-fi, tinkering
with mechanical keyboards, or trying to brew the perfect shot of espresso.
</p>
</div>
SkillsOutput
Triggered by: ./skills.sh
Renders animated ASCII progress bars grouped by category. Bars appear one by one at 150 ms intervals using a useState counter driven by a useEffect countdown.
Skill Data
const skills = [
{ name: "TypeScript", percent: 90, category: "Languages" },
{ name: "Python", percent: 85, category: "Languages" },
{ name: "Rust", percent: 60, category: "Languages" },
{ name: "React", percent: 95, category: "Frameworks" },
{ name: "Node.js", percent: 85, category: "Frameworks" },
{ name: "Next.js", percent: 80, category: "Frameworks" },
{ name: "Docker", percent: 75, category: "Tools" },
{ name: "Git", percent: 90, category: "Tools" },
{ name: "AWS", percent: 70, category: "Tools" },
];
Progress Bar Rendering
Each bar maps percent (0–100) to 20 filled/empty block characters:
const renderBar = (percent) => {
const filled = Math.round(percent / 100 * 20);
const empty = 20 - filled;
return (
<span className="text-terminal-green">
{"█".repeat(filled)}
<span className="text-terminal-dim">{"░".repeat(empty)}</span>
</span>
);
};
Animation
const [revealed, setRevealed] = useState(0);
useEffect(() => {
if (revealed < skills.length) {
const t = setTimeout(() => setRevealed((n) => n + 1), 150);
return () => clearTimeout(t);
}
}, [revealed]);
Skills are filtered so only those whose index is less than revealed are rendered. Categories without any revealed skills return null (no empty headers).
Layout Per Skill
TypeScript ██████████████████░░ 90%
Python █████████████████░░░ 85%
<div className="flex items-center">
<span className="w-24 text-terminal-white">{skill.name}</span>
<span className="mx-4">{renderBar(skill.percent)}</span>
<span className="text-terminal-white w-12 text-right">{skill.percent}%</span>
</div>
When all 9 skills have rendered, a final line appears:
Script execution completed successfully.
GitLogOutput
Triggered by: git log
Renders three career history entries in classic git log format.
Commit Data
const commits = [
{
hash: "a1b2c3d",
author: "guest <guest@portfolio.dev>",
date: "Mon Oct 24 09:00:00 2023 -0400",
message: `feat(career): Senior Frontend Engineer at TechCorp
- Led migration from Vue to React
- Mentored 3 junior developers
- Reduced bundle size by 40%`,
},
{
hash: "e4f5g6h",
author: "guest <guest@portfolio.dev>",
date: "Wed Jun 15 10:30:00 2021 -0400",
message: `feat(career): Full Stack Developer at StartupInc
- Built MVP from scratch using Next.js and Node
- Implemented CI/CD pipelines
- Handled 10k daily active users`,
},
{
hash: "i7j8k9l",
author: "guest <guest@portfolio.dev>",
date: "Mon Jan 10 08:15:00 2019 -0500",
message: `feat(career): Junior Web Developer at AgencyLLC
- Developed WordPress themes
- Created responsive landing pages
- Learned the hard way not to force push to main`,
},
];
Entry Layout
<div className="text-terminal-white">
<div className="text-terminal-amber">commit {hash}</div>
<div>Author: {author}</div>
<div>Date: {date}</div>
<div className="mt-2 ml-4 whitespace-pre-wrap">{message}</div>
</div>
Commit hashes are rendered in amber; the body is whitespace-pre-wrap so the multi-line message bullet points preserve their line breaks.
ManOutput
Triggered by: man <page>
Props: page: string (required)
Renders a Unix-style man page for the given page name. The component strips a .md extension if present before looking up the page key.
Currently one page is defined: migration-to-k8s
Page Structure
MIGRATION-TO-K8S(1) User Commands MIGRATION-TO-K8S(1)
NAME
migration-to-k8s - A case study on moving a monolith to Kubernetes
SYNOPSIS
migrate [OPTIONS] monolith microservices
DESCRIPTION
The migration-to-k8s project involved breaking down a 5-year-old Ruby
on Rails monolith into Go and Node.js microservices, orchestrated via
Kubernetes.
This resulted in a 40% reduction in infrastructure costs and improved
deployment frequency from bi-weekly to multiple times a day.
CHALLENGES
- Zero-downtime database migration using the strangler fig pattern.
- Training the existing engineering team on container orchestration.
- Setting up robust observability with Prometheus and Grafana.
The header and footer repeat the page name in the classic PAGE(1) format and a User Commands centre label. If the requested page has no entry, it renders:
No manual entry for <page>
MailOutput
Triggered by: mail [subject]
Props: subject?: string (defaults to "Hello")
A vim-inspired contact form with two editing modes, keyboard shortcut dispatch, and a simulated SMTP send flow.
Modes
| Mode | Behaviour |
|---|
INSERT | Textarea is editable; status bar shows -- INSERT -- |
NORMAL | Textarea is readOnly; status bar shows nothing (awaiting : commands) |
Keyboard Shortcuts
| Key | Context | Effect |
|---|
i | Normal mode | Switch to INSERT mode |
Escape | Any | Switch to NORMAL mode |
: | Normal mode | Begin command entry in status bar |
:wq + Enter | Normal mode | Trigger send (SMTP simulation) |
:q! + Enter | Normal mode | Abort and dismiss form |
SMTP Simulation
When :wq is submitted, the component enters a sending state and renders:
Resolving mx records for portfolio.dev...
Connecting to smtp.portfolio.dev:587...
TLS connection established.
Authenticating...
Sending message...
Please wait... ← amber
After 2 seconds the success state renders:
Message queued for delivery. Thank you! ← green
Layout
<div className="my-4 border border-terminal-dim p-2 flex flex-col h-64 bg-terminal-bg relative">
{/* Header */}
<div className="text-terminal-white border-b border-terminal-dim pb-1 mb-2 flex justify-between">
<span>To: guest@portfolio.dev</span>
<span>Subject: {subject}</span>
</div>
{/* Body */}
<textarea
className="flex-1 bg-transparent text-terminal-white outline-none resize-none font-mono"
readOnly={mode === "normal"}
onKeyDown={handleKeyDown}
/>
{/* Mode / command bar */}
<div className="mt-2 text-terminal-white flex justify-between items-center bg-terminal-dim px-2">
<span>{modeLabel}</span>
<span className="text-xs">Press ESC for normal mode, :wq to send, :q! to abort</span>
</div>
</div>
The textarea auto-focuses on mount when neither the sending nor aborted states are active.
MatrixRain
Triggered by: matrix command
A full-screen canvas overlay that renders falling Katakana + alphanumeric characters in the style of the Matrix digital rain. It auto-dismisses after 5 seconds — the Terminal component sets a showMatrix boolean to true on the matrix command and resets it with setTimeout(() => setShowMatrix(false), 5000).
Canvas Setup
const canvasRef = useRef(null);
useEffect(() => {
const canvas = canvasRef.current;
const ctx = canvas.getContext("2d");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const chars =
"アァカサタナハマヤャラワガザダバパ..." + // Katakana
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
"0123456789";
const fontSize = 16;
const columns = canvas.width / fontSize;
const drops = Array(Math.floor(columns)).fill(1);
const interval = setInterval(() => {
ctx.fillStyle = "rgba(0, 0, 0, 0.05)"; // fade trail
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = theme === "amber" ? "#ffb000" : "#39ff14";
ctx.font = `${fontSize}px monospace`;
drops.forEach((y, i) => {
const char = chars[Math.floor(Math.random() * chars.length)];
ctx.fillText(char, i * fontSize, y * fontSize);
if (y * fontSize > canvas.height && Math.random() > 0.975) {
drops[i] = 0;
}
drops[i]++;
});
}, 30);
// resize listener and cleanup
return () => clearInterval(interval);
}, [theme]);
The rain colour respects the active theme — #39ff14 (green) or #ffb000 (amber). The canvas sits at z-index: 50 (z-50) and uses pointer-events-none so clicks pass through to the terminal underneath.