Compare commits

...

11 Commits

15 changed files with 752 additions and 144 deletions
+38 -31
View File
@@ -1,11 +1,25 @@
import type { Metadata } from "next";
import { IconBrandLinkedinFilled, IconMailFilled } from "@tabler/icons-react";
import {
IconBrandLinkedinFilled,
IconMailFilled,
IconPhoneFilled,
} from "@tabler/icons-react";
import Hero from "@/components/content/Hero";
import SectionLabel from "@/components/layout/SectionLabel";
import AccentLink from "@/components/navigation/AccentLink";
import GiteaIcon from "@/components/icons/GiteaIcon";
import Tooltip from "@/components/ui/Tooltip";
import { colors, education, timeline } from "@/constants";
import CopyButton from "@/components/ui/CopyButton";
import {
about,
colors,
education,
email,
gitea,
linkedIn,
phone,
timeline,
} from "@/constants";
export const metadata: Metadata = {
title: "About — Angel Mankel",
@@ -18,23 +32,9 @@ export default function AboutPage() {
{/* Hero Section */}
<Hero label="About">
<div className="space-y-5 text-[19px] leading-[1.65]">
<p>
I came up through healthcare IT help desk, then Salesforce
administration, then software engineering. A path that taught me
to translate between operations, design, and engineering without
losing the thread.
</p>
<p>
These days I write React and TypeScript for production, lead
component-library work, and spend a lot of time in Claude Code
custom slash commands, MCP integrations, agent workflows. The
AI-tooling layer is reshaping how I work across the stack.
</p>
<p>
Based in Arizona, looking for remote-US roles where shipping
frontend product and building AI tooling around it are both
first-class.
</p>
{about.map((paragraph) => (
<p key={paragraph}>{paragraph}</p>
))}
</div>
</Hero>
@@ -68,22 +68,27 @@ export default function AboutPage() {
</ul>
</section>
<AccentLink link="/cv.pdf" label="Download CV (PDF)" />
<AccentLink link="/Angel_Mankel_Resume_2026.pdf" label="Download CV (PDF)" download />
{/* Social Links */}
<nav aria-label="Contact" className="flex gap-5 text-neutral-300">
<Tooltip text="Email">
<a
href="mailto:angelmankel@gmail.com"
aria-label="Email"
className={`hover:text-neutral-200 rounded-md hover:bg-[${colors.accent}] p-1 transition-colors duration-200`}
>
<IconMailFilled size={32} />
</a>
</Tooltip>
<CopyButton
value={email}
label="Copy email"
className={`hover:text-neutral-200 rounded-md hover:bg-[${colors.accent}] p-1 transition-colors duration-200`}
>
<IconMailFilled size={32} />
</CopyButton>
<CopyButton
value={phone}
label="Copy phone"
className={`hover:text-neutral-200 rounded-md hover:bg-[${colors.accent}] p-1 transition-colors duration-200`}
>
<IconPhoneFilled size={32} />
</CopyButton>
<Tooltip text="LinkedIn">
<a
href="https://www.linkedin.com/in/angel-mankel-0616aa132/"
href={linkedIn}
target="_blank"
rel="noreferrer"
aria-label="LinkedIn"
@@ -94,7 +99,9 @@ export default function AboutPage() {
</Tooltip>
<Tooltip text="Gitea">
<a
href="#"
href={gitea}
target="_blank"
rel="noreferrer"
aria-label="Gitea"
className={`hover:text-neutral-200 rounded-md hover:bg-[${colors.accent}] p-1 transition-colors duration-200`}
>
+6 -6
View File
@@ -14,20 +14,20 @@ export default function HomePage() {
<p className="font-serif italic text-3xl leading-[1.3]">
I&apos;m{" "}
<RotatingWord
items={["a developer", "a creative", "a tinkerer"]}
items={["a developer", "a creative", "a builder", "a tinkerer"]}
/>
.
</p>
<p className="text-[19px] leading-[1.65]">
Building software people rely on and understanding what makes those
people tick is what drives my greatest ambitions.
I build software people actually use, and I care about the people on
the other side of it as much as the code.
</p>
<p className="text-[19px] leading-[1.65]">
I&apos;m the kind of engineer who picks up whatever the problem needs and
keeps digging into the code, and into the people it&apos;s meant for.
The throughline isn&apos;t a stack, it&apos;s the curiosity.
I&apos;ll pick up whatever a problem needs and keep digging until it
works. The constant isn&apos;t a particular stack it&apos;s the
curiosity that got me here.
</p>
</div>
+130
View File
@@ -0,0 +1,130 @@
import type { Metadata } from "next";
import Image from "next/image";
import Link from "next/link";
import { notFound } from "next/navigation";
import { IconArrowNarrowLeft, IconExternalLink } from "@tabler/icons-react";
import Hero from "@/components/content/Hero";
import SectionLabel from "@/components/layout/SectionLabel";
import { colors, projects } from "@/constants";
export function generateStaticParams() {
return projects.map((project) => ({ slug: project.slug }));
}
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
const project = projects.find((p) => p.slug === slug);
if (!project) return { title: "Project — Angel Mankel" };
return {
title: `${project.title} — Angel Mankel`,
description: project.description,
};
}
export default async function ProjectPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const project = projects.find((p) => p.slug === slug);
if (!project) notFound();
return (
<div className="space-y-14">
<Link
href="/projects"
className="inline-flex items-center gap-1 text-sm text-neutral-400 transition-colors hover:text-neutral-200"
>
<IconArrowNarrowLeft size={18} />
All projects
</Link>
<Hero label={project.title} subtitle={project.description}>
<div className="space-y-5">
<p className="text-md text-neutral-400">
{project.year} {project.stack.join(" · ")}
</p>
<a
href={project.liveUrl}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-2 rounded-lg border border-neutral-700 px-4 py-2 text-[15px] font-medium transition-colors hover:border-neutral-500"
style={{ color: colors.accent }}
>
Open live app
<IconExternalLink size={18} />
</a>
</div>
</Hero>
{/* Screenshots */}
{project.screenshots.length > 0 && (
<section className="space-y-4">
{project.screenshots.map((shot) => (
<div
key={shot.src}
className="overflow-hidden rounded-xl border border-neutral-800"
>
<Image
src={shot.src}
alt={shot.alt}
width={1200}
height={750}
className="h-auto w-full"
/>
</div>
))}
</section>
)}
{/* Overview */}
<section className="space-y-4">
<SectionLabel label="Overview" />
<div className="space-y-4 text-[17px] leading-[1.65] text-neutral-200">
{project.overview.map((paragraph) => (
<p key={paragraph}>{paragraph}</p>
))}
</div>
</section>
{/* What I learned */}
<section className="space-y-4">
<SectionLabel label="What I learned" />
<ul className="space-y-2 text-[15px] leading-[1.65] text-neutral-200">
{project.learned.map((item) => (
<li
key={item}
className="before:mr-2 before:text-neutral-600 before:content-['']"
>
{item}
</li>
))}
</ul>
</section>
{/* What I'd do differently */}
<section className="space-y-4">
<SectionLabel label="What I'd do differently" />
<ul className="space-y-2 text-[15px] leading-[1.65] text-neutral-200">
{project.improvements.map((item) => (
<li
key={item}
className="before:mr-2 before:text-neutral-600 before:content-['']"
>
{item}
</li>
))}
</ul>
</section>
</div>
);
}
+119 -59
View File
@@ -1,77 +1,137 @@
import type { Metadata } from "next";
import Hero from "@/components/content/Hero";
import SectionLabel from "@/components/layout/SectionLabel";
import Collapsible from "@/components/ui/Collapsible";
import { hardware, editorAndShell, aiTools } from "@/constants";
import type { ToolGroup } from "@/constants/uses";
export const metadata: Metadata = {
title: "Uses — Angel Mankel",
};
type Section =
| { label: string; kind: "list"; items: string[] }
| { label: string; kind: "prose"; paragraphs: string[] };
const sections: Section[] = [
{
label: "Hardware",
kind: "list",
items: [
"Custom desktop — i9-12900H, 32GB RAM, Intel Iris Xe",
"Nobara Linux 43 · KDE Plasma 6 · Wayland",
"Headphones / keyboard / monitor — placeholder",
],
},
{
label: "Editor & Shell",
kind: "list",
items: [
"VS Code with a personal extension pack — placeholder",
"Bash / zsh — placeholder for shell",
"JetBrains Mono as the editor font",
"Theme — placeholder",
],
},
{
label: "AI Workflow",
kind: "prose",
paragraphs: [
"Claude Code is the daily driver — custom slash commands, MCP servers wired into my editor, and agent workflows that handle the repeat work so I can stay in the design problem.",
"Placeholder paragraph two — name a specific workflow and the time it saves.",
],
},
{
label: "Self-hosting",
kind: "list",
items: [
"Bare-metal home server, exposed to the internet through Traefik",
"This site lives on it — Next.js standalone build, no managed host",
"Other services — placeholder",
],
},
];
function ToolSection({
label,
groups,
}: {
label: string;
groups: ToolGroup[];
}) {
return (
<section className="space-y-4">
<SectionLabel label={label} />
<div className="space-y-3">
{groups.map((group) => (
<Collapsible
key={group.label}
title={group.label}
subtitle={`${group.items.length} items`}
>
<ul className="space-y-2 text-[14px]">
{group.items.map((item) => (
<li key={item.name} className="flex flex-col gap-0.5">
<span className="text-neutral-100">{item.name}</span>
{item.description && (
<span className="text-neutral-400">{item.description}</span>
)}
</li>
))}
</ul>
</Collapsible>
))}
</div>
</section>
);
}
export default function UsesPage() {
return (
<div className="space-y-14">
<Hero label="Uses" subtitle="What I reach for daily." />
<Hero label="Uses" subtitle="The gear, software, and setup behind my work." />
{sections.map((section) => (
<section key={section.label} className="space-y-4">
<SectionLabel label={section.label} />
{section.kind === "list" ? (
<ul className="space-y-2 text-[15px] leading-[1.65] text-neutral-200">
{section.items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
) : (
<div className="space-y-4 text-[15px] leading-[1.65] text-neutral-200">
{section.paragraphs.map((p) => (
<p key={p}>{p}</p>
<section className="space-y-4">
<SectionLabel label="Hardware" />
<div className="space-y-10">
{hardware.map((group) => (
<div key={group.label} className="space-y-4">
<h3 className="text-xs font-semibold uppercase tracking-[0.18em] text-neutral-500">
{group.label}
</h3>
<div className="space-y-3">
{group.machines.map((machine) => (
<Collapsible
key={machine.name}
title={machine.name}
subtitle={machine.role}
>
<dl className="space-y-3 text-[14px]">
<div className="flex flex-col gap-1 sm:flex-row sm:gap-3">
<dt className="w-20 shrink-0 text-neutral-500">OS</dt>
<dd className="text-neutral-200">{machine.os}</dd>
</div>
<div className="flex flex-col gap-1 sm:flex-row sm:gap-3">
<dt className="w-20 shrink-0 text-neutral-500">Specs</dt>
<dd className="flex flex-wrap gap-1.5">
{machine.specs.map((spec) => (
<span
key={spec}
className="rounded-md border border-neutral-800 bg-neutral-900 px-2 py-0.5 text-[12px] text-neutral-300"
>
{spec}
</span>
))}
</dd>
</div>
{machine.services && (
<div className="flex flex-col gap-1 sm:flex-row sm:gap-3">
<dt className="w-20 shrink-0 text-neutral-500">
Running
</dt>
<dd>
<ul className="space-y-1 text-neutral-200">
{machine.services.map((service) => (
<li
key={service}
className="before:mr-2 before:text-neutral-600 before:content-['']"
>
{service}
</li>
))}
</ul>
</dd>
</div>
)}
</dl>
</Collapsible>
))}
</div>
)}
</section>
))}
{group.extras?.map((extra) => (
<Collapsible
key={extra.label}
title={extra.label}
subtitle={`${extra.items.length} items`}
>
<ul className="space-y-1.5 text-[14px] text-neutral-200">
{extra.items.map((item) => (
<li
key={item}
className="before:mr-2 before:text-neutral-600 before:content-['']"
>
{item}
</li>
))}
</ul>
</Collapsible>
))}
</div>
))}
</div>
</section>
<ToolSection label="Editor & Shell" groups={editorAndShell} />
<ToolSection label="AI Tools" groups={aiTools} />
</div>
);
}
+22 -10
View File
@@ -1,23 +1,35 @@
import { IconArrowNarrowRight } from '@tabler/icons-react';
import Link from 'next/link';
import { IconArrowNarrowRight, IconExternalLink } from '@tabler/icons-react';
import { colors } from '@/constants';
interface ProjectCardProps {
title: string;
year: string;
stack: string[];
description: string;
outcome: string;
slug: string;
title: string;
year: string;
stack: string[];
description: string;
liveUrl: string;
}
export default function ProjectCard({ title, year, stack, description, outcome }: ProjectCardProps) {
export default function ProjectCard({ slug, title, year, stack, description, liveUrl }: ProjectCardProps) {
return (
<section className="space-y-2 border-t border-neutral-700 pt-4 flex flex-col mt-5">
<button className='flex justify-between hover:text-neutral-500 transition-colors items-center w-full text-left cursor-pointer group'>
<Link href={`/projects/${slug}`} className='flex justify-between hover:text-neutral-500 transition-colors items-center w-full text-left cursor-pointer group'>
<h1 className="text-[24px] font-semibold">{title}</h1>
<IconArrowNarrowRight className='group-hover:scale-150 transition-transform'/>
</button>
</Link>
<p className="text-md text-neutral-400 text-muted">{year} {stack.join(' · ')}</p>
<p className="text-xl">{description}</p>
<p className="text-md text-neutral-400 text-muted">{outcome}</p>
<a
href={liveUrl}
target="_blank"
rel="noreferrer"
className="mt-2 inline-flex w-fit items-center gap-2 rounded-lg border border-neutral-700 px-4 py-2 text-[15px] font-medium transition-colors hover:border-neutral-500"
style={{ color: colors.accent }}
>
Live Demo
<IconExternalLink size={18} />
</a>
</section>
);
}
+40 -1
View File
@@ -1,8 +1,47 @@
import {
IconBrandLinkedinFilled,
IconMailFilled,
IconPhoneFilled,
} from "@tabler/icons-react";
import GiteaIcon from "@/components/icons/GiteaIcon";
import Tooltip from "@/components/ui/Tooltip";
import CopyButton from "@/components/ui/CopyButton";
import { email, linkedIn, phone, gitea } from "@/constants";
export default function Footer() {
return (
<footer className="mx-auto w-full max-w-4xl px-6 py-6 flex items-center justify-between border-t border-neutral-800 text-xs text-neutral-500 sticky bottom-0 backdrop-blur-lg">
<span>© 2026 Angel Mankel</span>
<span>Self-hosted · Traefik · Next.js</span>
<nav aria-label="Contact" className="flex gap-3 text-neutral-600">
<CopyButton value={email} label="Copy email">
<IconMailFilled size={18} />
</CopyButton>
<CopyButton value={phone} label="Copy phone">
<IconPhoneFilled size={18} />
</CopyButton>
<Tooltip text="LinkedIn">
<a
href={linkedIn}
target="_blank"
rel="noreferrer"
aria-label="LinkedIn"
className="rounded-md p-1 transition-colors duration-200 hover:text-neutral-200"
>
<IconBrandLinkedinFilled size={18} />
</a>
</Tooltip>
<Tooltip text="Gitea">
<a
href={gitea}
target="_blank"
rel="noreferrer"
aria-label="Gitea"
className="rounded-md p-1 transition-colors duration-200 hover:text-neutral-200"
>
<GiteaIcon size={18} />
</a>
</Tooltip>
</nav>
</footer>
);
}
+2 -2
View File
@@ -1,9 +1,9 @@
import { IconArrowNarrowRight } from '@tabler/icons-react';
import { colors } from '@/constants';
export default function AccentLink({ link, label }: { link: string, label: string }) {
export default function AccentLink({ link, label, download }: { link: string, label: string, download?: boolean }) {
return (
<a href={link} className='flex gap-1 items-center hover:translate-x-2 hover:scale-110 transition-transform duration-200 w-fit'>
<a href={link} download={download} className='flex gap-1 items-center hover:translate-x-2 hover:scale-110 transition-transform duration-200 w-fit'>
<p style={{ color: colors.accent }}>{label}</p>
<IconArrowNarrowRight color={colors.accent} />
</a>
+56
View File
@@ -0,0 +1,56 @@
"use client";
import { useState, type ReactNode } from "react";
import { IconChevronDown } from "@tabler/icons-react";
type CollapsibleProps = {
title: ReactNode;
subtitle?: ReactNode;
defaultOpen?: boolean;
children: ReactNode;
};
export default function Collapsible({
title,
subtitle,
defaultOpen = false,
children,
}: CollapsibleProps) {
const [open, setOpen] = useState(defaultOpen);
return (
<div className="rounded-lg border border-neutral-800 bg-neutral-900/40 transition-colors hover:border-neutral-700">
<button
type="button"
onClick={() => setOpen((v) => !v)}
aria-expanded={open}
className="flex w-full items-center justify-between gap-4 px-4 py-3 text-left"
>
<div className="min-w-0">
<div className="text-[15px] font-medium text-neutral-100">
{title}
</div>
{subtitle && (
<div className="mt-0.5 text-xs text-neutral-400">{subtitle}</div>
)}
</div>
<IconChevronDown
size={18}
className={`shrink-0 text-neutral-500 transition-transform duration-200 ${
open ? "rotate-180" : ""
}`}
/>
</button>
<div
className={`grid transition-[grid-template-rows] duration-300 ease-out ${
open ? "grid-rows-[1fr]" : "grid-rows-[0fr]"
}`}
>
<div className="overflow-hidden">
<div className="border-t border-neutral-800 px-4 py-4">{children}</div>
</div>
</div>
</div>
);
}
+57
View File
@@ -0,0 +1,57 @@
"use client";
import { useState, type ReactNode } from "react";
type CopyButtonProps = {
value: string;
label: string;
copiedLabel?: string;
className?: string;
children: ReactNode;
};
export default function CopyButton({
value,
label,
copiedLabel = "Copied!",
className = "rounded-md p-1 text-neutral-600 transition-colors duration-200 hover:text-neutral-200",
children,
}: CopyButtonProps) {
const [copied, setCopied] = useState(false);
const handleCopy = async (e: React.MouseEvent<HTMLButtonElement>) => {
e.currentTarget.blur();
try {
await navigator.clipboard.writeText(value);
setCopied(true);
window.setTimeout(() => setCopied(false), 1500);
} catch {
// clipboard unavailable — silently ignore
}
};
return (
<span className="group relative inline-flex">
<button
type="button"
onClick={handleCopy}
aria-label={label}
className={className}
>
{children}
</button>
<span
role="tooltip"
className="
pointer-events-none absolute bottom-full left-1/2 mb-2 -translate-x-1/2
rounded-md bg-neutral-800 px-2 py-1 text-xs text-neutral-100
whitespace-nowrap opacity-0
transition-opacity duration-150
group-hover:opacity-100 group-focus-within:opacity-100
"
>
{copied ? copiedLabel : label}
</span>
</span>
);
}
+7
View File
@@ -0,0 +1,7 @@
export const about: string[] = [
"When I was about 10, I built a working gaming PC out of parts and a dead Dell tower. Taking things apart, figuring out how they worked, and putting them back together is what eventually pulled me into software development.",
"I started my career as an Audio Engineer in Los Angeles, CA — moved there at 18 years old and spent three years recording and mixing music. I loved it, but I realized the technical problem-solving was what I'd really been chasing all along.",
"I moved back to Arizona in 2016 to pursue a career in technology. Took an IT Help Desk job and taught myself Python and JavaScript. A few years later I was a Software Engineer building production web apps for a health tech company. Now I'm finishing my BA in Computer Science and heading straight into a Master's in AI.",
"Outside of work I build web apps for myself, develop games in Unreal Engine 5, and dig into AI pipelines — Claude Code is wired into most of my daily workflows.",
"I'm currently looking for US-based, remote AI-Native roles.",
];
+4
View File
@@ -0,0 +1,4 @@
export const email = "angelmankel@gmail.com"
export const linkedIn = "https://www.linkedin.com/in/angel-mankel-0616aa132/"
export const phone = "+1 (520) 709-0197"
export const gitea = "https://gitea.dev.blueoceanswim.com/donny/portfolio"
+4 -1
View File
@@ -2,4 +2,7 @@ export { colors } from "./colors";
export { testimonials } from "./testimonials";
export { projects } from "./projects";
export { education } from "./education";
export { timeline } from "./timeline";
export { timeline } from "./timeline";
export { hardware, editorAndShell, aiTools } from "./uses";
export { about } from "./about";
export { email, linkedIn, phone, gitea } from "./contact";
+112 -34
View File
@@ -1,44 +1,122 @@
export const projects = [
export type Screenshot = {
src: string;
alt: string;
};
export type Project = {
/** Route segment — the page lives at /projects/<slug> */
slug: string;
title: string;
year: string;
stack: string[];
/** Short summary shown on cards */
description: string;
/** Live demo URL */
liveUrl: string;
/** Longer write-up for the detail page (one entry per paragraph) */
overview: string[];
screenshots: Screenshot[];
/** What I learned building it */
learned: string[];
/** What I'd do differently next time */
improvements: string[];
};
export const projects: Project[] = [
{
title: "Project 1",
year: "2023",
stack: ["React", "Node.js"],
description: "A web application for managing tasks.",
outcome: "Increased productivity by 20%."
slug: "orbit",
title: "Orbit",
year: "2026",
stack: ["Next.js", "TypeScript", "PostgreSQL"],
description:
"A self-hosted project and ticket management workspace with spaces, pages, and notes.",
liveUrl: "https://demo.angelmankel.com/orbit",
overview: [
"TODO — what Orbit does and why you built it.",
"TODO — the core problem it solves and who it's for.",
],
screenshots: [
{ src: "https://picsum.photos/seed/orbit1/1200/750", alt: "Orbit board view" },
{ src: "https://picsum.photos/seed/orbit2/1200/750", alt: "Orbit ticket detail" },
],
learned: ["TODO — something you learned building this."],
improvements: ["TODO — what you'd do differently next time."],
},
{
title: "Project 2",
year: "2023",
stack: ["React", "Node.js"],
description: "A web application for managing tasks.",
outcome: "Increased productivity by 20%."
slug: "imagelab",
title: "ImageLab",
year: "2026",
stack: ["Next.js", "TypeScript", "WebAssembly"],
description:
"A browser-based image processing playground for transforms, filters, and batch edits.",
liveUrl: "https://demo.angelmankel.com/imagelab",
overview: [
"TODO — what ImageLab does and why you built it.",
"TODO — the core problem it solves and who it's for.",
],
screenshots: [
{ src: "https://picsum.photos/seed/imagelab1/1200/750", alt: "ImageLab editor" },
{ src: "https://picsum.photos/seed/imagelab2/1200/750", alt: "ImageLab batch view" },
],
learned: ["TODO — something you learned building this."],
improvements: ["TODO — what you'd do differently next time."],
},
{
title: "Project 3",
year: "2023",
stack: ["React", "Node.js"],
description: "A web application for managing tasks.",
outcome: "Increased productivity by 20%."
slug: "league",
title: "League of Legends Companion",
year: "2026",
stack: ["React", "TypeScript", "Riot API"],
description:
"An app I built for a friend who challenged themselves of reaching a higher rank. Surfaces specific matchup insights and personalized tips based on their match history.",
liveUrl: "https://demo.angelmankel.com/league",
overview: [
"TODO — what League of Legends Companion does and why you built it.",
"TODO — the core problem it solves and who it's for.",
],
screenshots: [
{ src: "https://picsum.photos/seed/league1/1200/750", alt: "League of Legends Companion overview" },
{ src: "https://picsum.photos/seed/league2/1200/750", alt: "League of Legends Companion match detail" },
],
learned: ["TODO — something you learned building this."],
improvements: ["TODO — what you'd do differently next time."],
},
{
title: "Project 4",
year: "2023",
stack: ["React", "Node.js"],
description: "A web application for managing tasks.",
outcome: "Increased productivity by 20%."
slug: "study",
title: "Study Time",
year: "2026",
stack: ["Next.js", "TypeScript", "Tailwind CSS"],
description:
"A study app I built for those late night study sessions - essentially a free Quizlet",
liveUrl: "https://demo.angelmankel.com/study",
overview: [
"TODO — what Study Time does and why you built it.",
"TODO — the core problem it solves and who it's for.",
],
screenshots: [
{ src: "https://picsum.photos/seed/study1/1200/750", alt: "Study Time dashboard" },
{ src: "https://picsum.photos/seed/study2/1200/750", alt: "Study Time session view" },
],
learned: ["TODO — something you learned building this."],
improvements: ["TODO — what you'd do differently next time."],
},
{
title: "Project 5",
year: "2023",
stack: ["React", "Node.js"],
description: "A web application for managing tasks.",
outcome: "Increased productivity by 20%."
slug: "d2r",
title: "Diablo II: Resurrected Tracker",
year: "2026",
stack: ["React", "TypeScript", "Node.js"],
description:
"A loot and event tracker for the game Diablo II: Resurrected.",
liveUrl: "https://demo.angelmankel.com/d2r",
overview: [
"TODO — what Diablo II: Resurrected Tracker does and why you built it.",
"TODO — the core problem it solves and who it's for.",
],
screenshots: [
{ src: "https://picsum.photos/seed/d2r1/1200/750", alt: "D2R Tracker grail view" },
{ src: "https://picsum.photos/seed/d2r2/1200/750", alt: "D2R Tracker item log" },
],
learned: ["TODO — something you learned building this."],
improvements: ["TODO — what you'd do differently next time."],
},
{
title: "Project 6",
year: "2023",
stack: ["React", "Node.js"],
description: "A web application for managing tasks.",
outcome: "Increased productivity by 20%."
}
]
];
+155
View File
@@ -0,0 +1,155 @@
export type Machine = {
name: string;
role: string;
specs: string[];
os: string;
services?: string[];
};
export type HardwareGroup = {
label: string;
machines: Machine[];
extras?: { label: string; items: string[] }[];
};
export type ToolEntry = {
name: string;
description?: string;
};
export type ToolGroup = {
label: string;
items: ToolEntry[];
};
export const hardware: HardwareGroup[] = [
{
label: "For work",
machines: [
{
name: "Minisforum MS-01",
role: "Primary workstation",
specs: ["Intel i9-12900H", "32GB DDR5", "4TB NVMe SSD"],
os: "Nobara Linux 43 · KDE Plasma 6 · Wayland",
services: [
"Traefik",
"Docker",
"Gitea",
"MetaMCP",
"20+ self-hosted development infra services",
],
},
{
name: "Minisforum N5 Pro",
role: "Home infrastructure",
specs: ["Ryzen AI 9 HX Pro 370", "32GB DDR5"],
os: "Proxmox v8.3.2",
services: [
"AdGuard LXC",
"Traefik",
"Ubuntu Docker VM",
"40+ self-hosted services",
],
},
{
name: "Custom workstation",
role: "GPU compute & creative work",
specs: [
"Ryzen 9 5900X",
"64GB DDR4",
"RTX A4500 20GB",
"Radeon Pro W7600",
],
os: "Windows 11",
},
],
extras: [
{
label: "Peripherals",
items: [
"TESmart 4-PC × 3-monitor KVM switch",
"Keychron Q6",
"Logitech PRO Superlight",
],
},
],
},
{
label: "For fun",
machines: [
{
name: "Gaming rig",
role: "Games & VR",
specs: ["Ryzen 7 5800X", "64GB DDR4", "RTX 3090 24GB"],
os: "Windows 11",
},
],
extras: [
{
label: "Displays",
items: [
"2× Gigabyte G27Q — 27\" 1440p 144Hz",
"1× ASUS VG289Q1A — 27\" 4K 60Hz",
],
},
],
},
];
export const editorAndShell: ToolGroup[] = [
{
label: "Editors",
items: [
{ name: "VS Code", description: "" },
{ name: "Zed", description: "" },
{ name: "Visual Studio", description: "" },
{ name: "JetBrains Rider", description: "" },
],
},
{
label: "Shell",
items: [
{ name: "Ghostty", description: "Terminal emulator on Linux" },
{ name: "Windows Terminal", description: "Terminal emulator on Windows" },
],
},
];
export const aiTools: ToolGroup[] = [
{
label: "Coding",
items: [
{
name: "Claude Code",
description:
"Daily driver — custom slash commands, MCP servers, agent workflows",
},
],
},
{
label: "Chat clients",
items: [
{ name: "Claude", description: "claude.ai web & desktop" },
{
name: "Open WebUI",
description: "Self-hosted chat UI in front of local & remote models",
},
],
},
{
label: "Local models",
items: [
{ name: "Ollama", description: "Local model runtime" },
],
},
{
label: "MCP & agents",
items: [
{
name: "MetaMCP",
description: "Self-hosted MCP server registry & router",
},
],
},
];
Binary file not shown.