232 lines
12 KiB
Markdown
232 lines
12 KiB
Markdown
# 👾 dvorakmortonxd.dev
|
|
|
|
> The source code for my personal website, **[dvorakmortonxd.dev](https://dvorakmortonxd.dev)**.
|
|
|
|
It's a **skeuomorphic personal site**: a dark homelab desk built from anodised aluminium, OLED
|
|
glass, carbon fibre and paper, with the whole KDE desktop simulation kept behind a physical
|
|
power switch. Two modes, one content set:
|
|
|
|
- **Web** (default) — the homelab desk. A 4U rack carries the headline on an engraved nameplate
|
|
and a live console readout; services are rack units with status LEDs, the 18 coding projects
|
|
are labelled SSD caddies in a drive drawer, hardware hangs on a pegboard, the contribution
|
|
heatmap is an LED matrix and the socials are a patch panel. Textures are CC0 — see
|
|
[`/credits`](http://localhost:3000/credits).
|
|
- **Desktop mode** — the KDE Plasma / Windows-style **desktop simulation**: draggable
|
|
snap-to-grid icons, resizable/floating windows, a Start menu, taskbar, live clock and a
|
|
working in-browser terminal (Alacritty). Reached from the power switch and hidden on phones.
|
|
|
|
Both modes surface the same personal content: socials, self-hosted services with **live
|
|
up/down health checks**, a hardware collection, coding projects, a **GitDXD contribution
|
|
heatmap**, gym SBD and links to friends' sites.
|
|
|
|
## ✨ Features
|
|
|
|
- **Web theme is the landing view** — the site always opens on the desk; the theme is
|
|
deliberately not persisted, so a reload never drops you into the desktop.
|
|
- **Skeuomorphic hardware** — rack, LEDs, rocker switch, screws, vents and panels are CSS
|
|
materials over five CC0 textures; every section is a real-world object rather than a card.
|
|
- **Desktop mode (desktop-only)** — a physical power switch in the top rail (and in the footer)
|
|
flips to the KDE-style desktop for the session. Phones never get it: the switch is hidden, so
|
|
the old iOS-style phone home screen is gone.
|
|
- **Draggable desktop icons** — free placement that snaps to a measured grid, one icon per
|
|
cell, with a follow-the-cursor drag ghost. Icons shrink on cramped viewports so they never
|
|
collide.
|
|
- **Multi-window manager** — draggable, resizable, minimizable, maximizable, focus/z-order
|
|
and animated open/close (with reduced-motion support). The taskbar shrinks its window
|
|
buttons as more apps open and scrolls when they still don't fit, so every open app stays
|
|
reachable. Windows are clamped to the viewport so they never open off-screen on a small
|
|
monitor and can't be dragged off any edge.
|
|
- **Working terminal app** — command input with history (`↑`/`↓`, `Ctrl+L` to clear) and a
|
|
hidden `ssh` easter egg.
|
|
- **Live service status** — each self-hosted service is health-checked through the site's
|
|
own API; Minecraft is checked through mcstatus.io.
|
|
- **Embedded live services** — opening a frameable self-hosted service (Llama.cpp/Open WebUI,
|
|
Jellyfin, Navidrome, GitDXD) from the desktop shows the real site inside the window (an
|
|
iframe). A service must not send a restrictive `X-Frame-Options`: Navidrome needs
|
|
`ND_HTTPHEADERS_FRAMEOPTIONS=ALLOWALL` and Forgejo needs `[cors] X_FRAME_OPTIONS = unset`.
|
|
A spinner covers the frame while the site boots. Discord sends `DENY` and keeps a link card
|
|
instead.
|
|
- **Friend sites in windows** — clicking a friend's desktop icon opens their site in a
|
|
window. `bukovinafilip.com` and `vlcak.me` send no framing headers, so they load directly;
|
|
`skopec.dev` sends `X-Frame-Options: SAMEORIGIN`, so it loads through `/api/frame`, a
|
|
header-stripping proxy that serves the live page from our origin. The `www` button always
|
|
opens the real site.
|
|
- **Minecraft-style SMP window** — the SMP shortcut opens a join menu modelled on
|
|
Minecraft's multiplayer screen, with live status, player count, MOTD and version pulled
|
|
from mcstatus.io through the site's own ping API.
|
|
- **Live GitDXD stats** — contribution heatmap + latest commit activity proxied server-side
|
|
from the self-hosted Forgejo instance.
|
|
- **Extras** — a playable `Revival Inc.` game embedded at `/revival-inc`, `llms.txt`,
|
|
`robots.txt` and a `sitemap.xml`.
|
|
|
|
## 🛠️ Tech Stack
|
|
|
|
- **Framework:** Next.js 16 (App Router, Turbopack)
|
|
- **Language:** TypeScript, React 19 (client components everywhere interactivity lives)
|
|
- **Styling:** `src/app/globals.css` holds the palette tokens, base styles, the Tailwind v4
|
|
`@theme`/`@utility` classes and the scroll reveals. `src/app/web.css` is the skeuomorphic
|
|
material system (metal, OLED glass, paper, carbon, rubber, plastic, LEDs, screws, vents,
|
|
switches) and the object layouts. `page.css` keeps the shared component styles the desktop
|
|
apps still use, and `skeuo.css` is the KDE desktop.
|
|
- **Fonts:** Newsreader (headlines, paper) + IBM Plex Sans/Mono (body, machine labels) via
|
|
`next/font/google`; the desktop keeps its system sans stack.
|
|
- **Textures:** five CC0 materials from ambientCG, downscaled to 512 px in `public/textures`.
|
|
Attribution is on `/credits`.
|
|
- **Version:** `3.1.0` (see `package.json`)
|
|
|
|
## 🚀 Running Locally
|
|
|
|
Requires **Node.js >= 20.9.0** (Next.js 16).
|
|
|
|
```bash
|
|
git clone https://git.dvorakmortonxd.dev/dvorakmortonxd/dvorakmortonxd.dev
|
|
cd dvorakmortonxd.dev
|
|
npm install
|
|
npm run dev
|
|
```
|
|
|
|
Then open [http://localhost:3000](http://localhost:3000).
|
|
|
|
### One-shot setup on Debian/Ubuntu
|
|
|
|
`install-dependencies-deb.sh` installs the system packages, Node.js LTS (>= 20.9.0) and npm,
|
|
then runs `npm install` and `npm run build`:
|
|
|
|
```bash
|
|
./install-dependencies-deb.sh # system deps + npm install + build
|
|
./install-dependencies-deb.sh --no-build # system deps + npm install only
|
|
./install-dependencies-deb.sh --no-npm # system packages only
|
|
```
|
|
|
|
### Scripts
|
|
|
|
```bash
|
|
npm run dev # node scripts/dev-server.mjs (dev + live updates)
|
|
npm run dev:next # next dev --turbopack (raw dev server, unprotected)
|
|
npm run build # next build --turbopack
|
|
npm run start # next start (production)
|
|
npm run lint # eslint
|
|
```
|
|
|
|
Handy checks before shipping: `npx tsc --noEmit`, `npx eslint <files>`, `npm run build`.
|
|
|
|
### Testing from a phone on your LAN
|
|
|
|
`next.config.ts` sets `allowedDevOrigins` so the dev server can be reached from other devices
|
|
on the local network and from the `beta.dvorakmortonxd.dev` tunnel. Without it Next.js
|
|
rejects the dev asset requests *and* the HMR WebSocket, which leaves the page un-hydrated
|
|
(stacked icons, dead buttons).
|
|
|
|
A plain `next dev` server is meant for localhost and exposes unauthenticated debug endpoints
|
|
(`/__nextjs*` — open-a-file-in-editor, stack frames, source maps, debugger attach, restart)
|
|
that you don't want on a tunnel. `npm run dev` therefore runs `scripts/dev-server.mjs`, a
|
|
custom server that keeps the app and live updates (HMR / Fast Refresh) but returns `404` for
|
|
those endpoints and `*.map` for any request that isn't a direct localhost connection.
|
|
Localhost keeps the full dev experience; the LAN and tunnel get a plain, live-reloading
|
|
site. To force the raw server instead, use `npm run dev:next` (don't point a tunnel at it).
|
|
|
|
### Running it as a service (survives reboot)
|
|
|
|
On this host the dev server runs as a **system** systemd unit, so it starts at boot on
|
|
its own — no login or linger needed. The unit lives in the repo at
|
|
[`deploy/dvorakmortonxd-dev.system.service`](./deploy/dvorakmortonxd-dev.system.service):
|
|
|
|
```bash
|
|
# installs /etc/systemd/system/dvorakmortonxd-dev.service and enables it
|
|
sudo bash deploy/install-system-service.sh # or: doas bash deploy/install-system-service.sh
|
|
```
|
|
|
|
Manage it with `systemctl status|restart|stop dvorakmortonxd-dev` and follow logs with
|
|
`journalctl -u dvorakmortonxd-dev -f`. It shares `.next/dev/lock` with a manual
|
|
`npm run dev`, so stop the service before starting one by hand.
|
|
|
|
<details>
|
|
<summary>No root? Use the per-user unit instead</summary>
|
|
|
|
[`deploy/dvorakmortonxd-dev.service`](./deploy/dvorakmortonxd-dev.service) is an equivalent
|
|
**user** unit you can install without root:
|
|
|
|
```bash
|
|
mkdir -p ~/.config/systemd/user
|
|
cp deploy/dvorakmortonxd-dev.service ~/.config/systemd/user/
|
|
systemctl --user daemon-reload
|
|
systemctl --user enable --now dvorakmortonxd-dev.service
|
|
loginctl enable-linger "$USER" # once, so it starts at boot
|
|
```
|
|
|
|
Drive it with `systemctl --user …` and `journalctl --user -u …`. Run only one of the two —
|
|
they contend for `:3000` and `.next/dev/lock`.
|
|
|
|
</details>
|
|
|
|
## 📡 API Routes
|
|
|
|
| Route | Method | What it does |
|
|
| --- | --- | --- |
|
|
| `/api/heatmap` | `GET` | Proxies the user's contribution heatmap from `git.dvorakmortonxd.dev`. |
|
|
| `/api/activities` | `GET` | Fetches the GitDXD activity feed and keeps only the user's own events. |
|
|
| `/api/frame?url=…` | `GET` | Header-stripping proxy for a friend's site that refuses framing (`skopec.dev`). **Allow-listed hosts only**; fetches the HTML server-side (so the site's `X-Frame-Options` never reaches the browser), injects a `<base>` for relative assets and a click shim for in-site links. Rejects other hosts, non-http(s) schemes and credentials, and never follows redirects. |
|
|
| `/api/ping?url=…` | `GET` | Health-checks an **allow-listed** service (the site's own `*.dvorakmortonxd.dev` hosts and the friends' sites). Handles `minecraft://host:port` via mcstatus.io; treats any non-5xx response as "up". Other hosts and schemes are rejected and redirects are never followed, so it can't be used to probe the LAN (SSRF guard). |
|
|
|
|
## 📁 Project Structure
|
|
|
|
```
|
|
src/app/
|
|
page.tsx → renders <ThemeRoot/>, imports the theme stylesheets
|
|
layout.tsx → html shell + metadata; loads the fonts; injects ThemeScript
|
|
globals.css → palette tokens, base, Tailwind @theme/@utility, scroll reveals
|
|
web.css → skeuomorphic material system + object layouts
|
|
page.css → shared component styles still used by the desktop apps
|
|
skeuo.css → KDE desktop styles
|
|
data.ts → socials, codingProjects, techProjects, friends, wallpapers
|
|
credits/page.tsx → credit page for the textures and fonts
|
|
revival-inc/page.tsx → embeds the bundled game at /public/revival-inc/index.html
|
|
api/
|
|
heatmap/route.ts → contribution heatmap proxy
|
|
activities/route.ts → GitDXD activity feed proxy
|
|
frame/route.ts → header-stripping proxy for unframeable friend sites
|
|
ping/route.ts → service health check (HTTP + Minecraft)
|
|
components/
|
|
ThemeRoot.tsx → web desk vs. Desktop mode
|
|
ThemeScript.tsx → pre-paint script: device detection
|
|
useTheme.ts → theme state via useSyncExternalStore
|
|
WebSite.tsx → the desk: rack hero, drawer, pegboard, patch panel
|
|
web-ui.tsx → rack units, LED dashboard, social masks, Reveal
|
|
WindowsDesktop.tsx → desktop shell: icons, windows, taskbar, Start menu
|
|
WinWindow.tsx → one draggable/resizable window
|
|
apps.tsx → APPS registry, window contents, in-app terminal
|
|
useDesktopIcons.ts → desktop icon grid + drag/drop logic
|
|
useDragOrder.ts → list reordering (used inside apps)
|
|
ServiceButton.tsx → desktop service link + live status dot
|
|
ContributionHeatmap.tsx → desktop heatmap + recent commits widget
|
|
Clock.tsx → live clock
|
|
JokerLogo.tsx → logo with click easter egg
|
|
scripts/
|
|
dev-server.mjs → custom dev server: live updates without the dev-only debug surface
|
|
deploy/
|
|
dvorakmortonxd-dev.system.service → systemd unit (system-wide) so the dev server starts at boot
|
|
dvorakmortonxd-dev.service → equivalent per-user unit (no root; needs linger)
|
|
install-system-service.sh → installs the system unit (run with sudo/doas)
|
|
public/ → fonts, wallpapers, icons, /revival-inc game, llms.txt, robots.txt, sitemap.xml
|
|
```
|
|
|
|
## 🌍 Deploying?
|
|
|
|
In order to deploy the website you'd want to set up a reverse proxy and something that runs it
|
|
as a service. Me personally, I use cloudflared and pm2. You could also use something like
|
|
Vercel, but I prefer self-hosting. Also, if you want to deploy it, you gotta change it enough
|
|
so that you don't directly copy my website, and also follow the GPL3.0-only license, meaning
|
|
you gotta make your code open source too.
|
|
|
|
Detailed step-by-step production instructions for **PM2 + npm on Debian and Alpine Linux**
|
|
(including the Cloudflare Tunnel / Nginx reverse-proxy setup) are in
|
|
[`DEPLOYMENT.md`](./DEPLOYMENT.md).
|
|
|
|
## 📬 Contact
|
|
|
|
To see my projects or get in touch, visit the live site or email me at
|
|
[dvorak@dvorakmortonxd.dev](mailto:dvorak@dvorakmortonxd.dev).
|
|
|
|
## 📄 License
|
|
|
|
GPL-3.0-only — see [`LICENSE`](./LICENSE).
|