rebuild the web theme as a skeuomorphic homelab desk (rack hero, drive drawer, LED dashboard, patch panel; CC0 textures + /credits)

This commit is contained in:
Martin Dvořák 2026-09-30 08:50:37 +00:00
commit dd6aeb5486
18 changed files with 1477 additions and 768 deletions

View file

@ -1,467 +0,0 @@
"use client";
import { useState } from "react";
import { useTheme } from "./useTheme";
import { codingProjects, friends, socials, techProjects } from "../data";
import {
Contributions,
Reveal,
Section,
SectionHeading,
ServiceRow,
SocialGlyph,
Stat,
TerminalCard,
type ServiceAccess,
} from "./editorial-ui";
/* --------------------------------------------------------------- content */
const NAV = [
{ href: "#about", label: "About" },
{ href: "#services", label: "Services" },
{ href: "#projects", label: "Projects" },
{ href: "#hardware", label: "Hardware" },
{ href: "#friends", label: "Friends" },
{ href: "#contact", label: "Contact" },
];
const SERVICES: {
name: string;
desc: string;
url: string;
access?: ServiceAccess;
checkUrl?: string;
disabled?: boolean;
}[] = [
{ name: "Proxmox", desc: "Server management — private.", url: "https://proxmox.dvorakmortonxd.dev/", access: "private", disabled: true },
{ name: "Llama.cpp", desc: "Local AI server on a GPU-passthrough RTX 3070.", url: "https://ai.dvorakmortonxd.dev/", access: "on_request" },
{ name: "Jellyfin", desc: "Self-hosted media streaming.", url: "https://jelly.dvorakmortonxd.dev/", access: "on_request" },
{ name: "SMP", desc: "My survival Minecraft server.", url: "https://discord.gg/vyvhk7d8bX", checkUrl: "minecraft://mc.dvorakmortonxd.dev:25565", access: "on_request" },
{ name: "Navidrome", desc: "My music, streamed from home.", url: "https://navidrome.dvorakmortonxd.dev/", access: "private" },
{ name: "GitDXD", desc: "My self-hosted Forgejo instance.", url: "https://git.dvorakmortonxd.dev", access: "public" },
];
const LIFTS = [
{ lift: "Squat", weight: "125 kg", date: "5 Jan 2025" },
{ lift: "Bench", weight: "111 kg", date: "29 May 2026" },
{ lift: "Deadlift", weight: "165 kg", date: "17 Dec 2024" },
];
/** Titles carry their state in parentheses, e.g. "dxdwm (Discontinued)". */
function splitStatus(title: string) {
const m = title.match(/^(.*?)\s*\(([^)]+)\)\s*$/);
return m ? { name: m[1], status: m[2] } : { name: title, status: null };
}
/* ------------------------------------------------------------------ page */
export default function EditorialSite() {
const { setTheme } = useTheme();
const [menuOpen, setMenuOpen] = useState(false);
const stats = [
{ value: String(SERVICES.length), label: "self-hosted services", sub: "git, media, music, AI, game server" },
{ value: String(codingProjects.length), label: "coding projects", sub: "games, tools, plugins, forks" },
{ value: String(techProjects.length), label: "machines collected", sub: "2009 – 2025, mostly e-waste" },
{ value: "401 kg", label: "best SBD total", sub: "squat 125 · bench 111 · deadlift 165" },
];
return (
<>
{/* ------------------------------------------------------------- nav */}
<header className="glass-bar fixed inset-x-0 top-0 z-50">
<div className="shell flex h-16 items-center gap-6">
<a href="#top" className="flex shrink-0 items-baseline gap-2.5" aria-label="Back to top">
<span className="display text-[1.05rem] tracking-tight">dvorakmortonxd.dev</span>
<span className="label hidden lg:inline">I use gentoo btw</span>
</a>
<nav className="ml-auto hidden items-center gap-7 md:flex" aria-label="Main">
{NAV.map((item) => (
<a key={item.href} href={item.href} className="text-sm text-muted transition-colors hover:text-ink">
{item.label}
</a>
))}
</nav>
<div className="ml-auto flex items-center gap-3 md:ml-0">
<button
type="button"
onClick={() => setTheme("skeuo")}
className="desktop-toggle glass-press-sm px-3 py-1.5 text-sm text-ink"
style={{ borderColor: "var(--rule-strong)" }}
>
Desktop mode
</button>
<button
type="button"
onClick={() => setMenuOpen((v) => !v)}
aria-expanded={menuOpen}
aria-label={menuOpen ? "Close menu" : "Open menu"}
className="glass-press-sm px-3 py-2 text-sm text-ink md:hidden"
>
{menuOpen ? "Close" : "Menu"}
</button>
</div>
</div>
{menuOpen ? (
<div className="animate-rise border-t border-[var(--rule-soft)] md:hidden">
<nav className="shell py-2" aria-label="Main">
{NAV.map((item) => (
<a
key={item.href}
href={item.href}
onClick={() => setMenuOpen(false)}
className="display block border-b border-[var(--rule-soft)] py-4 text-xl text-ink"
>
{item.label}
</a>
))}
</nav>
</div>
) : null}
</header>
<span id="top" />
<main className="pt-16">
{/* ---------------------------------------------------------- hero */}
<Section className="pb-16 pt-14 sm:pb-24 sm:pt-20">
<div className="grid grid-cols-1 items-start gap-14 lg:grid-cols-[1.05fr_1fr] lg:gap-20">
<Reveal>
<div className="flex flex-col items-start">
<p className="label">September 2026 &nbsp;·&nbsp; Czech Republic &nbsp;·&nbsp; IT student</p>
<h1 className="display mt-6 text-[2.6rem] leading-[1.06] sm:text-[3.4rem] lg:text-[3.9rem]">
I host my own internet, and I write code for <em>fun</em>.
</h1>
<p className="lede measure mt-7">
I’m Martin Dvořák — 17 years old, studying network management in Czechia. I self-host
everything from Git to music, collect old laptops, build small games, and lift when I’m
not doing any of that.
</p>
<div className="mt-9 flex flex-wrap items-center gap-x-8 gap-y-4">
<a
href="#services"
className="bg-[var(--accent)] px-5 py-3 text-sm font-medium text-[#1d2021] transition-colors hover:bg-[#ff9a3d]"
>
See what I run
</a>
<a href="#projects" className="link text-sm">
Read the projects
</a>
</div>
<p className="label mt-9">Self-hosted services &nbsp;·&nbsp; retro hardware &nbsp;·&nbsp; pygame game jams</p>
</div>
</Reveal>
<Reveal delay={100}>
<TerminalCard />
</Reveal>
</div>
</Section>
{/* ----------------------------------------------------------- stats */}
<Section className="pt-0 sm:pt-0">
<Reveal>
<div className="rule-strong grid grid-cols-1 gap-x-10 gap-y-8 pt-8 sm:grid-cols-2 lg:grid-cols-4">
{stats.map((s) => (
<Stat key={s.label} {...s} />
))}
</div>
</Reveal>
</Section>
{/* ----------------------------------------------------------- about */}
<Section id="about" className="scroll-mt-24">
<Reveal>
<SectionHeading
index="01"
eyebrow="About"
title={<>Who is behind the terminal</>}
lead="A student who would rather run the whole stack himself than rent it. The parts of it that are actually interesting:"
/>
</Reveal>
<Reveal delay={60}>
<div className="mt-12 grid grid-cols-1 gap-8 md:grid-cols-2 md:gap-16">
<p className="text-body">
I’m a sophomore at <a className="link" href="https://sssvt.cz" target="_blank" rel="noopener noreferrer">sssvt.cz</a>
, specializing in network management and administration. I wanted to go there since 7th
grade, so getting in was a good day. If you’re in middle school and into IT, it’s worth a
look.
</p>
<p className="text-body">
I’m a homelabber and self-hosting obsessive — I run my own Git, media, music, AI and game
servers. I write code for game jams, tinker with GNU/Linux and musl systems, and rescue old
hardware. My laptop collection spans 2009 to 2025; most of it came out of e-waste.
</p>
</div>
</Reveal>
</Section>
{/* ------------------------------------------------------------- now */}
<Section id="now" className="scroll-mt-24 pt-0 sm:pt-0">
<Reveal>
<SectionHeading
index="02"
eyebrow="Now"
title={<>What I’m doing right now</>}
lead="A snapshot, not a feed."
/>
</Reveal>
<Reveal delay={60}>
<div className="mt-12 grid grid-cols-1 gap-8 md:grid-cols-[1fr_1fr] md:gap-16">
<p className="text-body">
I’m hosting an SMP — message me on Discord if you want in. I’m also cooking most days,
bulking through the non-summer seasons.
</p>
<p className="text-body">
Me and my friend <a className="link" href="https://skopec.dev" target="_blank" rel="noopener noreferrer">Pepa</a>
{" "}are building an education app so we can study more efficiently. It’s called{" "}
<a className="link" href="https://git.dvorakmortonxd.dev/edu/maturita.c" target="_blank" rel="noopener noreferrer">
maturita.c
</a>
.
</p>
</div>
</Reveal>
</Section>
{/* -------------------------------------------------------- services */}
<Section id="services" className="scroll-mt-24">
<Reveal>
<SectionHeading
index="03"
eyebrow="Self-hosted"
title={<>Services I run from home</>}
lead="Live status is checked through this site. Some are private or shared on request — that’s what the label means."
/>
</Reveal>
<Reveal delay={60}>
<div className="mt-12">
<ul className="flex flex-col">
{SERVICES.map((s) => (
<li key={s.name}>
<ServiceRow {...s} />
</li>
))}
</ul>
</div>
</Reveal>
</Section>
{/* -------------------------------------------------------- projects */}
<Section id="projects" className="scroll-mt-24">
<Reveal>
<SectionHeading
index="04"
eyebrow={`${codingProjects.length} entries`}
title={<>Things I’ve built</>}
lead="Games, a browser, a window manager, a stack of abandoned servers and one very persistent Discord bot phase. Status in the margin."
/>
</Reveal>
<Reveal delay={60}>
<ol className="mt-12 flex flex-col">
{codingProjects.map((p, i) => {
const { name, status } = splitStatus(p.title);
return (
<li key={i} className="rule flex flex-col gap-2 py-5 md:flex-row md:gap-10">
<span className="label shrink-0 md:w-44 md:pt-1">
<span className="index-num">{String(i + 1).padStart(2, "0")}</span>
{status ? <span className="ml-3 text-faint">{status}</span> : null}
</span>
<div className="measure-wide min-w-0">
<a
href={p.url}
target="_blank"
rel="noopener noreferrer"
className="display text-lg transition-colors hover:text-accent"
>
{name}
</a>
<p className="mt-2 text-sm leading-relaxed text-body">{p.desc}</p>
</div>
</li>
);
})}
</ol>
</Reveal>
</Section>
{/* -------------------------------------------------------- hardware */}
<Section id="hardware" className="scroll-mt-24">
<Reveal>
<SectionHeading
index="05"
eyebrow={`${techProjects.length} machines`}
title={<>The hardware pile</>}
lead="From a 2009 ThinkPad to a 2025 MacBook Air — specs are whatever the machine actually shipped with."
/>
</Reveal>
<Reveal delay={60}>
<div className="mt-12 grid grid-cols-1 gap-x-16 gap-y-6 md:grid-cols-2">
{techProjects.map((t) => (
<div key={t.title} className="rule-soft pt-4">
<h3 className="display text-lg text-ink">{t.title}</h3>
<p className="mt-2 text-sm leading-relaxed text-body">{t.desc}</p>
</div>
))}
</div>
</Reveal>
</Section>
{/* ----------------------------------------------------- contributions */}
<Section id="gitdxd" className="scroll-mt-24">
<Reveal>
<SectionHeading
index="06"
eyebrow="GitDXD"
title={<>Commits, on my own forge</>}
lead={
<>
I run a Forgejo instance at{" "}
<a className="link" href="https://git.dvorakmortonxd.dev" target="_blank" rel="noopener noreferrer">
git.dvorakmortonxd.dev
</a>
. It started as a way to stop renting GitHub; it’s public now.
</>
}
/>
</Reveal>
<Reveal delay={60}>
<div className="mt-12 max-w-4xl">
<Contributions />
</div>
</Reveal>
</Section>
{/* --------------------------------------------------------- friends */}
<Section id="friends" className="scroll-mt-24">
<Reveal>
<SectionHeading
index="07"
eyebrow="Friends"
title={<>People worth visiting</>}
lead="Sites built by friends — go look at them."
/>
</Reveal>
<Reveal delay={60}>
<ul className="mt-12 flex flex-col">
{friends.map((f) => (
<li key={f.name} className="rule">
<a
href={f.realUrl || f.url}
target="_blank"
rel="noopener noreferrer"
className="group flex flex-wrap items-baseline justify-between gap-x-8 gap-y-1 py-5"
>
<span className="display text-lg transition-colors group-hover:text-accent">{f.name}</span>
<span className="label">{f.desc}</span>
</a>
</li>
))}
</ul>
</Reveal>
</Section>
{/* --------------------------------------------------------- lifting */}
<Section id="gym" className="scroll-mt-24">
<Reveal>
<SectionHeading
index="08"
eyebrow="Strength"
title={<>SBD, five days a week</>}
lead="A PPL split focused on strength. Current best singles at 73 kg bodyweight:"
/>
</Reveal>
<Reveal delay={60}>
<div className="rule-strong mt-12 grid grid-cols-1 gap-x-10 gap-y-8 pt-8 sm:grid-cols-3">
{LIFTS.map((l) => (
<Stat key={l.lift} value={l.weight} label={l.lift} sub={l.date} />
))}
</div>
</Reveal>
</Section>
{/* --------------------------------------------------------- contact */}
<Section id="contact" className="scroll-mt-24">
<Reveal>
<SectionHeading
index="09"
eyebrow="Contact"
title={<>Find me elsewhere</>}
lead={
<>
Fastest reply is Discord or Instagram. For anything longer, mail{" "}
<a className="link" href="mailto:dvorak@dvorakmortonxd.dev">
dvorak@dvorakmortonxd.dev
</a>
.
</>
}
/>
</Reveal>
<Reveal delay={60}>
<div className="rule-strong mt-12 grid grid-cols-2 gap-x-10 gap-y-3 pt-8 sm:grid-cols-3 lg:grid-cols-4">
{socials.map((s) => (
<a
key={s.name}
href={s.url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-3 py-1.5 text-sm text-body transition-colors hover:text-ink"
>
<SocialGlyph name={s.name} icon={s.icon} />
<span>{s.name}</span>
</a>
))}
</div>
</Reveal>
</Section>
</main>
{/* ---------------------------------------------------------- footer */}
<footer className="shell pb-14 pt-10">
<Reveal>
<div className="rule-strong pt-8">
<div className="flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
<div>
<div className="display text-xl">dvorakmortonxd.dev</div>
<p className="measure mt-2 text-sm text-body">
Built with Next.js. No trackers here — the same reason I run my own services.
</p>
</div>
<button
type="button"
onClick={() => setTheme("skeuo")}
className="desktop-toggle glass-press-sm shrink-0 px-3 py-1.5 text-sm text-ink"
>
Or open the desktop &rarr;
</button>
</div>
<div className="rule mt-10 flex flex-col gap-2 pt-5 text-xs text-faint sm:flex-row sm:justify-between">
<span>v3.0.0 · © 2025–2026 Martin Dvořák</span>
<span>Follow the GPL-3.0 license (or don’t, I don’t mind).</span>
</div>
</div>
</Reveal>
</footer>
</>
);
}

View file

@ -1,7 +1,7 @@
"use client";
import { useTheme } from "./useTheme";
import EditorialSite from "./EditorialSite";
import WebSite from "./WebSite";
import WindowsDesktop from "./WindowsDesktop";
// The web editorial site is the default and always the landing view (the theme
@ -17,7 +17,7 @@ export default function ThemeRoot() {
return (
<>
<div style={desktop ? { display: "none" } : undefined}>
<EditorialSite />
<WebSite />
</div>
{desktop ? <WindowsDesktop /> : null}
</>

View file

@ -0,0 +1,483 @@
"use client";
import { useState } from "react";
import { useTheme } from "./useTheme";
import { codingProjects, friends, socials, techProjects } from "../data";
import {
Bay,
ContributionsPanel,
Led,
Reveal,
ServiceUnit,
SocialGlyph,
type ServiceAccess,
} from "./web-ui";
/* --------------------------------------------------------------- content */
const NAV = [
{ href: "#about", label: "about" },
{ href: "#services", label: "rack" },
{ href: "#projects", label: "projects" },
{ href: "#hardware", label: "inventory" },
{ href: "#contact", label: "ports" },
];
const SERVICES: {
name: string;
desc: string;
url: string;
access?: ServiceAccess;
checkUrl?: string;
disabled?: boolean;
}[] = [
{ name: "proxmox", desc: "server management — private console", url: "https://proxmox.dvorakmortonxd.dev/", access: "private", disabled: true },
{ name: "llama.cpp", desc: "local models on a passthrough RTX 3070", url: "https://ai.dvorakmortonxd.dev/", access: "on_request" },
{ name: "jellyfin", desc: "media library, streamed from home", url: "https://jelly.dvorakmortonxd.dev/", access: "on_request" },
{ name: "minecraft smp", desc: "survival server on mc.dvorakmortonxd.dev", url: "https://discord.gg/vyvhk7d8bX", checkUrl: "minecraft://mc.dvorakmortonxd.dev:25565", access: "on_request" },
{ name: "navidrome", desc: "my music, no streaming subscription", url: "https://navidrome.dvorakmortonxd.dev/", access: "private" },
{ name: "gitdxd", desc: "self-hosted Forgejo forge", url: "https://git.dvorakmortonxd.dev", access: "public" },
];
const LIFTS = [
{ lift: "squat", weight: "125", unit: "kg", date: "05 Jan 2025" },
{ lift: "bench", weight: "111", unit: "kg", date: "29 May 2026" },
{ lift: "deadlift", weight: "165", unit: "kg", date: "17 Dec 2024" },
];
const NEOFETCH: [string, string][] = [
["host", "Proxmox VE · home rack"],
["os", "Gentoo Linux (x86_64)"],
["cpu", "Xeon E5-2680 v4 · 14c/28t"],
["gpu", "RTX 3070 · passthrough"],
["memory", "68 GB DDR4 RDIMM"],
["storage", "256 GB NVMe · 1 TB · 750 GB"],
["services", "6 containers, 0 subscriptions"],
["uptime", "17 years"],
];
/** Titles carry their state in parentheses, e.g. "dxdwm (Discontinued)". */
function splitStatus(title: string) {
const m = title.match(/^(.*?)\s*\(([^)]+)\)\s*$/);
return m ? { name: m[1], status: m[2] } : { name: title, status: null };
}
function statusKind(status: string | null): "green" | "amber" | "red" | "off" {
if (!status) return "off";
const s = status.toLowerCase();
if (s.includes("finished") || s.includes("available")) return "green";
if (s.includes("wip") || s.includes("unfinished")) return "amber";
return "red";
}
/* ------------------------------------------------------------------ page */
export default function WebSite() {
const { setTheme } = useTheme();
const [menuOpen, setMenuOpen] = useState(false);
const stats = [
{ value: String(SERVICES.length), label: "self-hosted services" },
{ value: String(codingProjects.length), label: "projects on the shelf" },
{ value: String(techProjects.length), label: "machines logged" },
{ value: "401", label: "kg best SBD total" },
];
return (
<div className="w-root">
{/* ----------------------------------------------------------- rail */}
<header className="w-rail w-metal-flat" style={{ borderRadius: 0 }}>
<div className="shell flex h-16 items-center gap-5">
<a href="#top" className="flex shrink-0 items-center gap-3" aria-label="Back to top">
<Led kind="green" pulse />
<span className="w-rack__title">dvorakmortonxd.local</span>
</a>
<nav className="ml-auto hidden items-center gap-6 md:flex" aria-label="Main">
{NAV.map((item) => (
<a
key={item.href}
href={item.href}
className="w-navlink"
>
{item.label}
</a>
))}
</nav>
<div className="ml-auto flex items-center gap-3 md:ml-0">
<button
type="button"
aria-label="Switch to Desktop mode"
data-desktop-mode="off"
onClick={() => setTheme("skeuo")}
className="desktop-toggle flex items-center gap-3"
title="Flip to Desktop mode"
>
<span className="w-plate">desktop</span>
<span className="w-switch">
<span className="w-switch__knob" />
<Led kind="off" className="w-switch__led" />
</span>
</button>
<div className="md:hidden">
<button
type="button"
onClick={() => setMenuOpen((v) => !v)}
aria-expanded={menuOpen}
aria-label={menuOpen ? "Close menu" : "Open menu"}
className="w-btn px-3 py-2"
>
{menuOpen ? "close" : "menu"}
</button>
</div>
</div>
</div>
{menuOpen ? (
<nav className="shell w-metal-flat mb-3 mt-1 flex flex-col gap-1 p-3 md:hidden" aria-label="Main">
{NAV.map((item) => (
<a
key={item.href}
href={item.href}
onClick={() => setMenuOpen(false)}
className="w-unit__name block border-b border-[rgba(255,255,255,0.06)] py-3"
>
{item.label}
</a>
))}
</nav>
) : null}
</header>
<span id="top" />
<main className="pt-16">
{/* --------------------------------------------------------- hero */}
<section className="shell pt-12 pb-14 sm:pt-16">
<Reveal>
<div className="w-rack w-metal">
<div className="w-rack__head">
<Led kind="green" pulse />
<span className="w-rack__title">dvorakmortonxd.local</span>
<span className="w-unit__meta ml-auto hidden sm:block">4U · asus p9x79 · 24/7</span>
<span className="w-screw ml-2 hidden sm:block" />
<span className="w-screw hidden sm:block" />
</div>
<div className="grid gap-3 lg:grid-cols-[minmax(0,1.3fr)_minmax(0,1fr)]">
<div className="w-nameplate min-w-0">
<p className="w-kicker">homelab · czech republic · online</p>
<h1 className="mt-4 text-[2rem] leading-[1.05] sm:text-[2.6rem] lg:text-[3rem]">
My own little internet — one rack, zero subscriptions.
</h1>
<p className="w-prose mt-5 max-w-[48ch]">
I’m Martin Dvořák. Git, media, music, an AI box and a Minecraft server all run on a
Proxmox machine in my room — plus the hardware graveyard I keep rescuing from e-waste.
</p>
<div className="mt-7 flex flex-wrap items-center gap-3">
<a href="#services" className="w-btn w-btn--accent">
<Led kind="amber" /> see what I run
</a>
<a href="#projects" className="w-btn">
open the drive drawer
</a>
</div>
</div>
<div className="w-panel min-w-0 p-4">
<div className="w-rack__head !border-b-0 !pb-2">
<span className="w-rack__title">console — neofetch</span>
<Led kind="green" className="ml-auto" />
</div>
<div className="w-screen-text">
{NEOFETCH.map(([k, v]) => (
<p key={k} className="flex gap-3">
<span className="k w-[8ch] shrink-0">{k}</span>
<span className="v min-w-0 truncate">{v}</span>
</p>
))}
<p className="mt-1">
<span className="k">$</span> <span className="w-cursor" />
</p>
</div>
</div>
</div>
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
{stats.map((s) => (
<div key={s.label} className="w-unit w-metal-flat">
<Led kind="amber" />
<span className="min-w-0">
<span className="w-digits block text-[1.5rem] leading-none">{s.value}</span>
<span className="w-unit__meta mt-1 block truncate">{s.label}</span>
</span>
</div>
))}
</div>
</div>
</Reveal>
</section>
<div className="shell flex flex-col gap-16 pb-20 sm:gap-24">
{/* -------------------------------------------------------- about */}
<Bay id="about" index="01" label="about" title="Who is behind the rack">
<div className="grid gap-6 md:grid-cols-2">
<Reveal delay={40}>
<article className="w-paper w-card h-full">
<div className="flex items-center gap-3">
<span className="w-grommet" />
<span className="w-kicker !text-[var(--paper-ink-soft)]">manifest.txt</span>
</div>
<p className="w-prose mt-4">
I’m a sophomore at{" "}
<a className="w-link" href="https://sssvt.cz" target="_blank" rel="noopener noreferrer">
sssvt.cz
</a>
, studying network management and administration. I wanted to go there since 7th
grade, so getting in was a good day.
</p>
<p className="w-prose mt-3">
I’m a homelabber first: my own Git, media, music, AI and game servers, most of it
running on hardware other people threw away.
</p>
</article>
</Reveal>
<Reveal delay={130}>
<article className="w-paper w-card w-note--tilt h-full">
<div className="flex items-center gap-3">
<span className="w-grommet" />
<span className="w-kicker !text-[var(--paper-ink-soft)]">inventory.txt</span>
</div>
<p className="w-prose mt-4">
A laptop collection from 2009 to 2025, mostly e-waste rescues — ThinkPads, MacBooks,
a Legion Go, an Xbox with a bad-update exploit and one very dead 2010 unibody.
</p>
<p className="w-prose mt-3">
Away from the rack: PPL training 3–5× a week, Terraria (~1300 h), Minecraft modpacks
and the occasional game jam.
</p>
</article>
</Reveal>
</div>
</Bay>
{/* ---------------------------------------------------------- now */}
<Bay id="now" index="02" label="now" title="On the bench right now">
<Reveal delay={40}>
<div className="grid gap-4 lg:grid-cols-[minmax(0,1.25fr)_minmax(0,1fr)]">
<article className="w-paper w-note min-w-0">
<span className="w-kicker !text-[var(--paper-ink-soft)]">now.txt · updated 09/2026</span>
<p className="w-prose mt-4">
I’m hosting an SMP — message me on Discord if you want in. I’m also cooking most days,
bulking through the non-summer seasons.
</p>
<p className="w-prose mt-3">
Me and my friend{" "}
<a className="w-link" href="https://skopec.dev" target="_blank" rel="noopener noreferrer">
Pepa
</a>{" "}
are building an education app so we can study more efficiently. It’s called{" "}
<a className="w-link" href="https://git.dvorakmortonxd.dev/edu/maturita.c" target="_blank" rel="noopener noreferrer">
maturita.c
</a>
.
</p>
</article>
<div className="w-panel min-w-0 p-4">
<div className="w-rack__head !border-b-0 !pb-2">
<span className="w-rack__title">status</span>
<Led kind="green" pulse className="ml-auto" />
</div>
<div className="w-screen-text">
<p><span className="k">smp</span> <span className="v">accepting players</span></p>
<p><span className="k">bulk</span> <span className="v">in progress</span></p>
<p><span className="k">maturita.c</span> <span className="v">in development</span></p>
<p><span className="k">gym</span> <span className="v">PPL · 3–5×/week</span></p>
</div>
</div>
</div>
</Reveal>
</Bay>
{/* ----------------------------------------------------- services */}
<Bay id="services" index="03" label="rack" title="Rack elevation — live status">
<Reveal delay={40}>
<div className="w-rack w-metal">
<div className="w-rack__head">
<span className="w-rack__title">services</span>
<span className="w-unit__meta ml-auto">checked through this site</span>
</div>
{SERVICES.map((s) => (
<ServiceUnit key={s.name} {...s} />
))}
</div>
</Reveal>
</Bay>
{/* ----------------------------------------------------- projects */}
<Bay id="projects" index="04" label="projects" title="The drive drawer">
<Reveal delay={40}>
<div className="w-drawer w-metal">
<div className="w-drawer__handle mx-auto mb-4 w-40" />
<div className="max-h-[560px] overflow-y-auto pr-1">
<div className="grid gap-2 sm:grid-cols-2 xl:grid-cols-3">
{codingProjects.map((p, i) => {
const { name, status } = splitStatus(p.title);
return (
<a
key={i}
href={p.url}
target="_blank"
rel="noopener noreferrer"
className="w-caddy w-carbon"
title={p.desc}
>
<Led kind={statusKind(status)} />
<span className="min-w-0 flex-1">
<span className="w-caddy__label block truncate">{name}</span>
<span className="w-caddy__status block truncate">
{status ?? "unlabelled"} · {p.desc}
</span>
</span>
</a>
);
})}
</div>
</div>
</div>
</Reveal>
</Bay>
{/* ----------------------------------------------------- hardware */}
<Bay id="hardware" index="05" label="inventory" title="On the pegboard">
<Reveal delay={40}>
<div className="w-pegboard w-metal">
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
{techProjects.map((t) => (
<article key={t.title} className="w-paper w-tag">
<span className="w-tag__ring" />
<h3 className="w-serif text-[1rem]">{t.title}</h3>
<p className="w-prose mt-2 text-[0.8rem]">{t.desc}</p>
</article>
))}
</div>
</div>
</Reveal>
</Bay>
{/* -------------------------------------------------- contributions */}
<Bay id="gitdxd" index="06" label="gitdxd" title="Forge telemetry">
<Reveal delay={40}>
<ContributionsPanel />
</Reveal>
</Bay>
{/* -------------------------------------------------------- friends */}
<Bay id="friends" index="07" label="friends" title="Cards on the desk">
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{friends.map((f, i) => (
<Reveal key={f.name} delay={40 + i * 70}>
<a
href={f.realUrl || f.url}
target="_blank"
rel="noopener noreferrer"
className={`w-paper w-card block transition-transform hover:-translate-y-1 ${i % 2 ? "w-note--tilt" : ""}`}
>
<h3 className="w-serif text-[1.1rem]">{f.name}</h3>
<p className="mt-1 font-mono text-[0.72rem] text-[var(--paper-ink-soft)]">{f.desc}</p>
</a>
</Reveal>
))}
</div>
</Bay>
{/* ------------------------------------------------------------ gym */}
<Bay id="gym" index="08" label="strength" title="On the platform">
<Reveal delay={40}>
<div className="w-metal p-5">
<div className="grid gap-3 sm:grid-cols-3">
{LIFTS.map((l) => (
<div key={l.lift} className="w-panel p-4 text-center">
<div className="w-digits text-[2.2rem] leading-none">
{l.weight}
<span className="ml-1 text-[1rem] opacity-70">{l.unit}</span>
</div>
<div className="w-kicker mt-3">{l.lift}</div>
<div className="w-unit__meta mt-1">{l.date} · 73 kg BW</div>
</div>
))}
</div>
</div>
</Reveal>
</Bay>
{/* -------------------------------------------------------- contact */}
<Bay id="contact" index="09" label="ports" title="Patch panel">
<Reveal delay={40}>
<div className="w-patch w-metal">
<div className="w-rack__head">
<span className="w-rack__title">socials</span>
<span className="w-unit__meta ml-auto">12 ports</span>
</div>
<div className="mt-4 grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
{socials.map((s) => (
<a key={s.name} href={s.url} target="_blank" rel="noopener noreferrer" className="w-port">
<span className="w-port__jack" />
<span className="text-[var(--faint)]">
<SocialGlyph name={s.name} icon={s.icon} />
</span>
<span className="w-port__label min-w-0 truncate">{s.name}</span>
<Led kind="green" className="ml-auto" />
</a>
))}
</div>
<p className="w-unit__meta mt-5">
fastest reply is Discord or Instagram · mail{" "}
<a className="w-link" href="mailto:dvorak@dvorakmortonxd.dev">
dvorak@dvorakmortonxd.dev
</a>
</p>
</div>
</Reveal>
</Bay>
</div>
</main>
{/* --------------------------------------------------------- footer */}
<footer className="shell pb-14 pt-10">
<Reveal>
<div className="w-metal-flat flex flex-col gap-5 p-5 md:flex-row md:items-center md:justify-between">
<div className="min-w-0">
<div className="flex items-center gap-3">
<Led kind="green" />
<span className="w-rack__title">dvorakmortonxd.local</span>
</div>
<p className="w-unit__meta mt-2">
built with Next.js · textures CC0 via ambientCG ·{" "}
<a className="w-link" href="/credits">
credits
</a>
</p>
</div>
<button
type="button"
onClick={() => setTheme("skeuo")}
className="w-btn desktop-toggle shrink-0"
title="Flip to Desktop mode"
>
<Led kind="amber" /> desktop mode
</button>
</div>
<div className="mt-4 flex flex-col gap-1 font-mono text-[0.68rem] text-[var(--faint)] sm:flex-row sm:justify-between">
<span>v3.1.0 · © 2025–2026 Martin Dvořák</span>
<span>GPL-3.0-only — follow it or don’t, I don’t mind</span>
</div>
</Reveal>
</footer>
</div>
);
}

View file

@ -22,7 +22,6 @@ export function Reveal({
el.classList.add("is-in");
return;
}
const io = new IntersectionObserver(
(entries) => {
for (const e of entries) {
@ -56,90 +55,62 @@ export function Reveal({
);
}
/* ------------------------------------------------------------ primitives */
/* ------------------------------------------------------------- hardware */
export function Section({
id,
children,
export function Led({
kind = "green",
pulse = false,
className = "",
}: {
id?: string;
children: ReactNode;
className?: string;
}) {
return (
<section id={id} className={`shell relative py-14 sm:py-20 ${className}`}>
{children}
</section>
);
}
export function SectionHeading({
index,
eyebrow,
title,
lead,
}: {
index?: string;
eyebrow?: string;
title: ReactNode;
lead?: ReactNode;
kind?: "green" | "amber" | "red" | "blue" | "off";
pulse?: boolean;
className?: string;
title?: string;
}) {
return (
<header className="rule pt-5">
<div className="flex flex-col gap-3 md:flex-row md:gap-10">
<div className="label shrink-0 md:w-44 md:pt-1">
{index ? <span className="text-accent">{index}</span> : null}
{index && eyebrow ? <span className="px-1.5 text-faint">/</span> : null}
{eyebrow}
</div>
<div className="measure-wide min-w-0">
<h2 className="display text-[1.75rem] sm:text-[2.125rem]">{title}</h2>
{lead ? <p className="lede mt-4">{lead}</p> : null}
</div>
</div>
</header>
);
}
export function Stat({ value, label, sub }: { value: string; label: string; sub?: string }) {
return (
<div className="rule pt-4">
<div className="serif-num text-[2rem] leading-none sm:text-[2.5rem]">{value}</div>
<div className="mt-3 text-sm leading-snug text-body">{label}</div>
{sub ? <div className="label mt-1.5 block">{sub}</div> : null}
</div>
);
}
/* -------------------------------------------------------------- socials */
/** Minimal inline LinkedIn mark (the bundled linkedin.svg is a filled badge,
* which masks to a plain square). Everything else masks its SVG. */
function LinkedInGlyph({ className = "size-4" }: { className?: string }) {
return (
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" className={className}>
<path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.77-1.95 4.03 0 4.78 2.5 4.78 5.75V21h-4v-5.6c0-1.34-.03-3.06-1.95-3.06-1.95 0-2.25 1.45-2.25 2.96V21H9z" />
</svg>
);
}
export function SocialGlyph({ name, icon }: { name: string; icon: string }) {
if (name === "LinkedIn") return <LinkedInGlyph />;
return (
<span
className={`w-led w-led--${kind}${pulse ? " w-led--pulse" : ""} ${className}`}
title={title}
aria-hidden="true"
className="social-mask size-4"
style={{ WebkitMaskImage: `url(${icon})`, maskImage: `url(${icon})` } as CSSProperties}
/>
);
}
/** Section header, mounted on the chassis. */
export function Bay({
id,
index,
label,
title,
children,
}: {
id: string;
index: string;
label: string;
title: ReactNode;
children: ReactNode;
}) {
return (
<section id={id} className="scroll-mt-28">
<Reveal>
<div className="flex flex-wrap items-center gap-x-4 gap-y-3">
<span className="w-plate">
{index} <span className="opacity-50">/</span> {label}
</span>
<h2 className="w-serif text-[1.6rem] sm:text-[2rem]">{title}</h2>
</div>
</Reveal>
<div className="mt-8">{children}</div>
</section>
);
}
/* ------------------------------------------------------------- services */
export type ServiceAccess = "public" | "private" | "on_request";
export function ServiceRow({
export function ServiceUnit({
name,
desc,
url,
@ -173,10 +144,9 @@ export function ServiceRow({
}, [url, checkUrl, disabled]);
const effective = disabled ? "up" : status;
const dot =
effective === "up" ? "var(--positive)" : effective === "down" ? "var(--negative)" : "var(--faint)";
const kind = effective === "up" ? "green" : effective === "down" ? "red" : "off";
const statusLabel = disabled
? "private"
? "standby"
: effective === "up"
? "online"
: effective === "down"
@ -185,34 +155,47 @@ export function ServiceRow({
const accessLabel =
access === "public" ? "public" : access === "private" ? "private" : access === "on_request" ? "on request" : null;
const body = (
const host = (() => {
try {
return new URL(url).host;
} catch {
return url;
}
})();
const inner = (
<>
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-baseline gap-x-3 gap-y-1">
<span className="display text-lg">{name}</span>
{accessLabel ? <span className="label">{accessLabel}</span> : null}
</div>
<p className="mt-1 text-sm text-body">{desc}</p>
</div>
<span className="flex shrink-0 items-center gap-2">
<span className="size-1.5 rounded-full" style={{ backgroundColor: dot }} />
<span className="label">{statusLabel}</span>
<span className="w-unit__leds">
<Led kind={kind} pulse={effective === "up"} />
<Led kind="amber" pulse={effective === "up"} />
</span>
<span className="min-w-0 flex-1">
<span className="w-unit__name block truncate">{name}</span>
<span className="w-unit__meta block truncate">
{host} {accessLabel ? `· ${accessLabel}` : ""}
</span>
</span>
<span className="hidden text-right sm:block">
<span className="w-unit__meta block max-w-[26ch] truncate">{desc}</span>
</span>
<span className="w-plate shrink-0">
<Led kind={kind} /> {statusLabel}
</span>
</>
);
const cls = "rule-soft flex items-center gap-4 py-4 md:gap-8";
const cls = "w-unit w-metal-flat";
if (disabled) {
return (
<span className={cls} aria-disabled="true">
{body}
{inner}
</span>
);
}
return (
<a href={url} target="_blank" rel="noopener noreferrer" className={`${cls} transition-colors hover:bg-[var(--fill)]`}>
{body}
<a href={url} target="_blank" rel="noopener noreferrer" className={`${cls} w-unit--link`}>
{inner}
</a>
);
}
@ -230,9 +213,7 @@ interface Activity {
repo?: { full_name?: string };
}
const LEVEL_BG = ["rgba(235,219,178,0.07)", "#6d7420", "#98971a", "#b8bb26", "#d8e05a"];
export function Contributions() {
export function ContributionsPanel() {
const [weeks, setWeeks] = useState<{ date: string; contribs: number; level: number }[][]>([]);
const [total, setTotal] = useState(0);
const [activities, setActivities] = useState<Activity[]>([]);
@ -310,108 +291,99 @@ export function Contributions() {
};
}, []);
if (state === "loading") return <p className="label">Loading contributions…</p>;
if (state === "error") return <p className="label">Could not load contributions.</p>;
// level 4 = brightest LED, level 0 = off
const levelOn = (l: number) => (l <= 0 ? "0" : String(5 - l));
return (
<div>
<div className="flex flex-wrap items-end justify-between gap-4">
<div className="serif-num text-[2rem] leading-none sm:text-[2.5rem]">{total}</div>
<div className="label">contributions in the last year</div>
<div className="w-dashboard w-metal">
<div className="w-rack__head">
<span className="w-rack__title">gitdxd.activity</span>
<span className="ml-auto flex items-center gap-2">
<Led kind={state === "error" ? "red" : "green"} pulse={state === "ready"} />
<span className="w-unit__meta">{state === "error" ? "link down" : "streaming"}</span>
</span>
</div>
<div className="ed-heatmap mt-6 pb-2">
{weeks.map((w, i) => (
<div key={i} className="ed-heatmap-week">
{w.map((d) => (
<div
key={d.date}
className="ed-heatmap-cell"
style={{ backgroundColor: LEVEL_BG[d.level] }}
title={`${d.contribs} on ${d.date}`}
/>
<div className="mt-4 flex flex-wrap items-end justify-between gap-3">
<div className="w-digits text-[2rem] leading-none sm:text-[2.5rem]">{total}</div>
<div className="w-kicker">contributions / last year</div>
</div>
{state === "loading" ? (
<p className="w-screen-text mt-5 opacity-70">reading matrix…</p>
) : state === "error" ? (
<p className="w-screen-text mt-5 opacity-70">could not reach the forge.</p>
) : (
<>
<div className="w-ledgrid mt-5">
{weeks.map((w, i) => (
<div key={i} className="w-ledgrid__col">
{w.map((d) => (
<div key={d.date} className="w-ledgrid__cell" data-on={levelOn(d.level)} title={`${d.contribs} on ${d.date}`} />
))}
</div>
))}
</div>
))}
</div>
<div className="mt-3 flex items-center justify-end gap-2">
<span className="w-unit__meta">less</span>
{[4, 3, 2, 1, 0].map((l) => (
<span key={l} className="w-ledgrid__cell" data-on={levelOn(l)} />
))}
<span className="w-unit__meta">more</span>
</div>
<div className="mt-3 flex items-center justify-end gap-2">
<span className="label">less</span>
<span className="ed-heatmap-legend">
{LEVEL_BG.map((bg, i) => (
<span key={i} className="ed-heatmap-cell" style={{ backgroundColor: bg }} />
))}
</span>
<span className="label">more</span>
</div>
{activities.length > 0 ? (
<ul className="mt-8 flex flex-col">
{activities.map((a, i) => {
let msg = "commit";
try {
if (a.content) {
const parsed = JSON.parse(a.content) as {
Commits?: { Message?: string }[];
HeadCommit?: { Message?: string };
};
msg =
parsed.Commits?.[0]?.Message?.split("\n")[0] ??
parsed.HeadCommit?.Message?.split("\n")[0] ??
msg;
}
} catch {
/* keep default */
}
return (
<li key={i} className="rule-soft flex flex-wrap items-baseline justify-between gap-x-6 gap-y-1 py-3">
<span className="min-w-0 text-sm text-ink">
<span className="text-muted">{a.repo?.full_name || "unknown"}</span> — {msg}
</span>
<span className="label shrink-0">{new Date(a.created).toLocaleDateString()}</span>
</li>
);
})}
</ul>
) : null}
{activities.length > 0 ? (
<div className="w-panel mt-5 p-3">
<div className="w-screen-text">
{activities.map((a, i) => {
let msg = "commit";
try {
if (a.content) {
const parsed = JSON.parse(a.content) as {
Commits?: { Message?: string }[];
HeadCommit?: { Message?: string };
};
msg =
parsed.Commits?.[0]?.Message?.split("\n")[0] ??
parsed.HeadCommit?.Message?.split("\n")[0] ??
msg;
}
} catch {
/* keep default */
}
return (
<p key={i} className="truncate">
<span className="k">$</span> {a.repo?.full_name || "unknown"} — {msg}
</p>
);
})}
</div>
</div>
) : null}
</>
)}
</div>
);
}
/* ---------------------------------------------------------- terminal card */
/* ------------------------------------------------------------- socials */
export function TerminalCard() {
const rows: [string, string][] = [
["OS", "Gentoo Linux (x86_64)"],
["WM", "SwayFX"],
["Shell", "zsh"],
["Editor", "neovim"],
["Services", "6 self-hosted"],
["Gym", "SBD 401 kg @ 73 kg"],
["Uptime", "17 years"],
];
/** The bundled linkedin.svg is a filled badge, which masks to a plain square. */
function LinkedInGlyph({ className = "size-4" }: { className?: string }) {
return (
<div className="glass-sunken overflow-hidden">
<div className="flex items-center gap-2 border-b border-[var(--rule-soft)] px-4 py-2.5">
<span className="size-2.5 rounded-full bg-[var(--rule)]" />
<span className="size-2.5 rounded-full bg-[var(--rule)]" />
<span className="size-2.5 rounded-full bg-[var(--rule)]" />
<span className="label ml-2">martin@dvorakmortonxd</span>
</div>
<div className="space-y-1.5 p-5 font-mono text-[0.78rem] leading-relaxed text-body">
<p className="text-ink">
<span className="text-accent">$</span> neofetch
</p>
{rows.map(([k, v]) => (
<p key={k} className="flex gap-3">
<span className="w-20 shrink-0 text-accent">{k.toLowerCase()}</span>
<span>{v}</span>
</p>
))}
<p className="text-ink">
<span className="text-accent">$</span> <span className="animate-blink">█</span>
</p>
</div>
</div>
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" className={className}>
<path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.77-1.95 4.03 0 4.78 2.5 4.78 5.75V21h-4v-5.6c0-1.34-.03-3.06-1.95-3.06-1.95 0-2.25 1.45-2.25 2.96V21H9z" />
</svg>
);
}
export function SocialGlyph({ name, icon }: { name: string; icon: string }) {
if (name === "LinkedIn") return <LinkedInGlyph />;
return (
<span
aria-hidden="true"
className="social-mask size-4"
style={{ WebkitMaskImage: `url(${icon})`, maskImage: `url(${icon})` } as CSSProperties}
/>
);
}