Skip to main content

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
man 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

ModeBehaviour
INSERTTextarea is editable; status bar shows -- INSERT --
NORMALTextarea is readOnly; status bar shows nothing (awaiting : commands)

Keyboard Shortcuts

KeyContextEffect
iNormal modeSwitch to INSERT mode
EscapeAnySwitch to NORMAL mode
:Normal modeBegin command entry in status bar
:wq + EnterNormal modeTrigger send (SMTP simulation)
:q! + EnterNormal modeAbort 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.

Build docs developers (and LLMs) love