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

@ -0,0 +1,389 @@
"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>
);
}
/* ------------------------------------------------------------- hardware */
export function Led({
kind = "green",
pulse = false,
className = "",
title,
}: {
kind?: "green" | "amber" | "red" | "blue" | "off";
pulse?: boolean;
className?: string;
title?: string;
}) {
return (
<span
className={`w-led w-led--${kind}${pulse ? " w-led--pulse" : ""} ${className}`}
title={title}
aria-hidden="true"
/>
);
}
/** 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 ServiceUnit({
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 kind = effective === "up" ? "green" : effective === "down" ? "red" : "off";
const statusLabel = disabled
? "standby"
: effective === "up"
? "online"
: effective === "down"
? "offline"
: "checking";
const accessLabel =
access === "public" ? "public" : access === "private" ? "private" : access === "on_request" ? "on request" : null;
const host = (() => {
try {
return new URL(url).host;
} catch {
return url;
}
})();
const inner = (
<>
<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 = "w-unit w-metal-flat";
if (disabled) {
return (
<span className={cls} aria-disabled="true">
{inner}
</span>
);
}
return (
<a href={url} target="_blank" rel="noopener noreferrer" className={`${cls} w-unit--link`}>
{inner}
</a>
);
}
/* -------------------------------------------------------- contributions */
interface HeatmapPoint {
timestamp: number;
contributions: number;
}
interface Activity {
op_type: string;
content?: string | null;
created: string;
repo?: { full_name?: string };
}
export function ContributionsPanel() {
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;
};
}, []);
// level 4 = brightest LED, level 0 = off
const levelOn = (l: number) => (l <= 0 ? "0" : String(5 - l));
return (
<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="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 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>
{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>
);
}
/* ------------------------------------------------------------- socials */
/** The bundled linkedin.svg is a filled badge, which masks to a plain square. */
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}
/>
);
}