rework the site: gruvbox editorial default with a desktop-mode toggle; drop the terminal layout and phone desktop

This commit is contained in:
Martin Dvořák 2026-09-30 07:49:31 +00:00
commit 86692637de
19 changed files with 1350 additions and 1664 deletions

View file

@ -0,0 +1,453 @@
"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">
<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>
</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>
<div className="mt-12">
<ul className="flex flex-col">
{SERVICES.map((s) => (
<li key={s.name}>
<ServiceRow {...s} />
</li>
))}
</ul>
</div>
</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>
<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>
</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>
<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>
</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>
<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>
</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>
<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>
</Section>
</main>
{/* ---------------------------------------------------------- footer */}
<footer className="shell pb-14 pt-10">
<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>
</footer>
</>
);
}