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,417 +0,0 @@
"use client";
import { useEffect, useRef, useState, type CSSProperties, type ReactNode } from "react";
/* --------------------------------------------------------------- reveal */
export function Reveal({
children,
delay = 0,
className = "",
}: {
children: ReactNode;
delay?: number;
className?: string;
}) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
el.classList.add("is-in");
return;
}
const io = new IntersectionObserver(
(entries) => {
for (const e of entries) {
if (e.isIntersecting) {
el.classList.add("is-in");
io.disconnect();
}
}
},
{ rootMargin: "0px 0px -12% 0px", threshold: 0.08 }
);
const rect = el.getBoundingClientRect();
const onScreen = rect.top < window.innerHeight && rect.bottom > 0;
if (onScreen) {
// First screenful: stagger in document order so it fills top to bottom.
const all = Array.from(document.querySelectorAll<HTMLElement>(".reveal"));
const order = Math.min(Math.max(0, all.indexOf(el)), 8);
el.style.setProperty("--delay", `${delay + order * 90}ms`);
requestAnimationFrame(() => el.classList.add("is-in"));
} else {
io.observe(el);
}
return () => io.disconnect();
}, [delay]);
return (
<div ref={ref} className={`reveal ${className}`} style={{ "--delay": `${delay}ms` } as CSSProperties}>
{children}
</div>
);
}
/* ------------------------------------------------------------ primitives */
export function Section({
id,
children,
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;
}) {
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
aria-hidden="true"
className="social-mask size-4"
style={{ WebkitMaskImage: `url(${icon})`, maskImage: `url(${icon})` } as CSSProperties}
/>
);
}
/* ------------------------------------------------------------- services */
export type ServiceAccess = "public" | "private" | "on_request";
export function ServiceRow({
name,
desc,
url,
access,
checkUrl,
disabled,
}: {
name: string;
desc: string;
url: string;
access?: ServiceAccess;
checkUrl?: string;
disabled?: boolean;
}) {
const [status, setStatus] = useState<"pending" | "up" | "down">("pending");
useEffect(() => {
if (disabled) return;
let alive = true;
fetch(`/api/ping?url=${encodeURIComponent(checkUrl || url)}`)
.then((r) => r.json())
.then((d) => {
if (alive) setStatus(d.status === "up" ? "up" : "down");
})
.catch(() => {
if (alive) setStatus("down");
});
return () => {
alive = false;
};
}, [url, checkUrl, disabled]);
const effective = disabled ? "up" : status;
const dot =
effective === "up" ? "var(--positive)" : effective === "down" ? "var(--negative)" : "var(--faint)";
const statusLabel = disabled
? "private"
: effective === "up"
? "online"
: effective === "down"
? "offline"
: "checking";
const accessLabel =
access === "public" ? "public" : access === "private" ? "private" : access === "on_request" ? "on request" : null;
const body = (
<>
<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>
</>
);
const cls = "rule-soft flex items-center gap-4 py-4 md:gap-8";
if (disabled) {
return (
<span className={cls} aria-disabled="true">
{body}
</span>
);
}
return (
<a href={url} target="_blank" rel="noopener noreferrer" className={`${cls} transition-colors hover:bg-[var(--fill)]`}>
{body}
</a>
);
}
/* -------------------------------------------------------- contributions */
interface HeatmapPoint {
timestamp: number;
contributions: number;
}
interface Activity {
op_type: string;
content?: string | null;
created: string;
repo?: { full_name?: string };
}
const LEVEL_BG = ["rgba(235,219,178,0.07)", "#6d7420", "#98971a", "#b8bb26", "#d8e05a"];
export function Contributions() {
const [weeks, setWeeks] = useState<{ date: string; contribs: number; level: number }[][]>([]);
const [total, setTotal] = useState(0);
const [activities, setActivities] = useState<Activity[]>([]);
const [state, setState] = useState<"loading" | "ready" | "error">("loading");
useEffect(() => {
let alive = true;
(async () => {
try {
const res = await fetch("/api/heatmap");
if (!res.ok) throw new Error("bad status");
const data: HeatmapPoint[] = await res.json();
const map = new Map<string, number>();
let sum = 0;
let max = 0;
for (const item of data) {
const d = new Date(item.timestamp * 1000);
const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
const v = (map.get(key) ?? 0) + item.contributions;
map.set(key, v);
if (v > max) max = v;
sum += item.contributions;
}
const end = new Date();
end.setHours(0, 0, 0, 0);
const start = new Date(end);
start.setFullYear(start.getFullYear() - 1);
start.setDate(start.getDate() - start.getDay());
const out: { date: string; contribs: number; level: number }[][] = [];
let week: { date: string; contribs: number; level: number }[] = [];
for (const d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
const contribs = map.get(key) ?? 0;
let level = 0;
if (contribs > 0) {
level = 1;
if (contribs > max * 0.25) level = 2;
if (contribs > max * 0.5) level = 3;
if (contribs > max * 0.75) level = 4;
}
week.push({ date: key, contribs, level });
if (week.length === 7) {
out.push(week);
week = [];
}
}
if (week.length) out.push(week);
if (!alive) return;
setWeeks(out);
setTotal(sum);
setState("ready");
} catch {
if (alive) setState("error");
}
})();
(async () => {
try {
const res = await fetch("/api/activities");
if (!res.ok) return;
const data: Activity[] = await res.json();
if (alive) {
setActivities(data.filter((d) => d.op_type === "commit_repo" || d.op_type === "push_repo").slice(0, 3));
}
} catch {
/* ignore */
}
})();
return () => {
alive = false;
};
}, []);
if (state === "loading") return <p className="label">Loading contributions…</p>;
if (state === "error") return <p className="label">Could not load contributions.</p>;
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>
<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>
))}
</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}
</div>
);
}
/* ---------------------------------------------------------- terminal card */
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"],
];
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>
);
}