From 86692637de7e6b2f16cf854787d93576aeb015d1 Mon Sep 17 00:00:00 2001 From: Martin Dvorak Date: Wed, 30 Sep 2026 07:49:31 +0000 Subject: [PATCH] rework the site: gruvbox editorial default with a desktop-mode toggle; drop the terminal layout and phone desktop --- README.md | 73 ++-- agents.md | 82 ++-- package.json | 2 +- src/app/api/ping/route.ts | 4 +- src/app/components/EditorialSite.tsx | 453 +++++++++++++++++++ src/app/components/MobileHome.tsx | 232 ---------- src/app/components/SlBackground.tsx | 106 ----- src/app/components/TerminalLayout.tsx | 603 ------------------------- src/app/components/ThemeRoot.tsx | 29 +- src/app/components/ThemeScript.tsx | 5 +- src/app/components/apps.tsx | 2 +- src/app/components/editorial-ui.tsx | 405 +++++++++++++++++ src/app/components/useTheme.ts | 13 +- src/app/editorial.css | 52 +++ src/app/globals.css | 300 ++++++++++++- src/app/layout.tsx | 33 +- src/app/page.css | 6 +- src/app/page.tsx | 1 + src/app/skeuo.css | 607 +------------------------- 19 files changed, 1347 insertions(+), 1661 deletions(-) create mode 100644 src/app/components/EditorialSite.tsx delete mode 100644 src/app/components/MobileHome.tsx delete mode 100644 src/app/components/SlBackground.tsx delete mode 100644 src/app/components/TerminalLayout.tsx create mode 100644 src/app/components/editorial-ui.tsx create mode 100644 src/app/editorial.css diff --git a/README.md b/README.md index d0a1674..fd8214c 100644 --- a/README.md +++ b/README.md @@ -2,27 +2,30 @@ > The source code for my personal website, **[dvorakmortonxd.dev](https://dvorakmortonxd.dev)**. -This is not a normal portfolio page. It ships **two fully interactive shells** and picks -which one to show based on the saved theme and the device, all from the same content: +It's an **editorial-style personal site** in a **gruvbox** palette, with the whole KDE +desktop simulation kept behind a "Desktop mode" button. Two modes, one content set: -- **`skeuo`** (default) — a KDE Plasma / Windows-style **desktop simulation**: draggable - snap-to-grid icons, resizable/floating windows, a Start menu, taskbar and live clock. - On phones it becomes an **iOS-style home screen** with a widget grid, dock, drag-to-reorder - and an iOS launch animation. -- **`terminal`** — a **Gruvbox TUI** version of the site: TUI panels, an animated ASCII - train background, a clickable easter-egg logo, and a working in-browser terminal (Alacritty) - with commands like `help`, `ls`, `cat`, `neofetch`, `emerge`, `ssh` and more. +- **Gruvbox editorial** (default) — a technical-print layout: serif headlines, mono labels in + the margin, hairline rules and a data strip. No images at all; the only graphics are the + social icons, used as monochrome CSS masks. +- **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). Reachable from the "Desktop mode" button and hidden + on phones. -Both shells surface the same personal content: socials, self-hosted services with **live +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**, a gym SBD panel and links to friends' sites. +heatmap**, gym SBD and links to friends' sites. ## ✨ Features -- **Two themes / shells** — KDE-style desktop and Gruvbox terminal, toggled from either UI - and persisted in `localStorage` (applied pre-paint to avoid a flash). -- **Phone vs. desktop detection** — a user-agent sniff on `` decides whether - the desktop or the mobile home screen is shown. +- **Gruvbox editorial by default** — a technical-print layout: serif headlines, mono labels in + the margin, hairline rules, a data strip. Text and type only — the sole graphics are the + social icons, rendered as monochrome CSS masks. +- **Desktop mode (desktop-only)** — a "Desktop mode" button swaps to the KDE-style desktop; + the choice persists in `localStorage` (applied pre-paint to avoid a flash). Phones never get + it: the pre-paint script forces gruvbox and CSS hides the button, 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. @@ -58,13 +61,14 @@ heatmap**, a gym SBD panel and links to friends' sites. - **Framework:** Next.js 16 (App Router, Turbopack) - **Language:** TypeScript, React 19 (client components everywhere interactivity lives) -- **Styling:** hand-written CSS — `src/app/page.css` (terminal theme), `src/app/skeuo.css` - (desktop + mobile), `src/app/globals.css` (base/reset). Tailwind v4 is imported for the - base layer but the UI is custom CSS. -- **Fonts:** Monocraft (self-hosted in `public/`, used by the terminal theme; the desktop - theme uses the system sans stack) -- **Images:** `next/image` -- **Version:** `2.1.0` (see `package.json`) +- **Styling:** `src/app/globals.css` is the gruvbox editorial design system (CSS custom + properties, Tailwind v4 `@theme`/`@utility`, base + scroll reveals). `editorial.css` holds + page helpers; `page.css` keeps the shared component styles the desktop apps still use, and + `skeuo.css` is the KDE desktop. The editorial page itself is built from Tailwind utilities. +- **Fonts:** Newsreader (display) + IBM Plex Sans/Mono (body/labels) via `next/font/google`; + the desktop keeps its system sans stack. +- **Images:** `next/image` — but the editorial page uses none (social SVGs as monochrome masks only). +- **Version:** `3.0.0` (see `package.json`) ## 🚀 Running Locally @@ -164,33 +168,34 @@ they contend for `:3000` and `.next/dev/lock`. ``` src/app/ - page.tsx → renders , imports the two theme stylesheets - layout.tsx → html shell + metadata, injects ThemeScript - globals.css → base/reset styles + Tailwind import - page.css → terminal theme styles - skeuo.css → desktop + mobile home styles + page.tsx → renders , imports the theme stylesheets + layout.tsx → html shell + metadata; loads the fonts; injects ThemeScript + globals.css → gruvbox editorial design system (tokens, @utility classes, base) + editorial.css → editorial page helpers (social masks, heatmap) + page.css → shared component styles still used by the desktop apps + skeuo.css → KDE desktop styles data.ts → socials, codingProjects, techProjects, friends, wallpapers 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 → picks which shell(s) to mount + ThemeRoot.tsx → gruvbox editorial vs. Desktop mode ThemeScript.tsx → pre-paint script: theme + device detection useTheme.ts → theme state via useSyncExternalStore - TerminalLayout.tsx → the Gruvbox terminal shell + EditorialSite.tsx → the gruvbox editorial page (nav, hero, sections) + editorial-ui.tsx → editorial primitives: Section, ServiceRow, Contributions, … WindowsDesktop.tsx → desktop shell: icons, windows, taskbar, Start menu - MobileHome.tsx → phone shell: iOS home screen + app view 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 (mobile grid/dock) - ServiceButton.tsx → service link + live status dot - ContributionHeatmap.tsx → heatmap + recent commits widget + 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 - SlBackground.tsx → animated ASCII train background scripts/ dev-server.mjs → custom dev server: live updates without the dev-only debug surface deploy/ diff --git a/agents.md b/agents.md index d947705..3017000 100644 --- a/agents.md +++ b/agents.md @@ -33,16 +33,18 @@ When in doubt, prefer updating. A stale README **or** agents.md is treated as a ## What this project is -Personal portfolio site for **dvorakmortonxd.dev**. It's not a normal website — -it ships **two fully interactive desktop/OS simulations** plus a terminal, and -picks which one to show based on theme and device. +Personal portfolio site for **dvorakmortonxd.dev**. The default view is a +**gruvbox editorial page** (a technical-print layout, no images); a "Desktop mode" +button swaps to a **KDE Plasma desktop simulation**. Phones only ever get the +editorial page. - Framework: **Next.js 16** (App Router, Turbopack), **React 19**, TypeScript. -- Styling: plain CSS files (`page.css`, `skeuo.css`), no CSS-in-JS, no Tailwind - utilities in the components (Tailwind is present via PostCSS but the look is - hand-written CSS). +- Styling: **Tailwind v4** for the editorial page — `globals.css` is the design + system (CSS custom properties, `@theme` tokens, `@utility` classes) — plus plain + CSS for the desktop (`skeuo.css`) and the shared component styles the desktop + apps still use (`page.css`). - Path alias: `@/*` → `src/*`. -- Version: `2.1.0` (see `package.json`). +- Version: `3.0.0` (see `package.json`). ### Scripts ```bash @@ -73,12 +75,13 @@ token.** Do not print it, copy it into new files, or push it anywhere else. ``` src/app/ - page.tsx → renders , imports page.css + skeuo.css - layout.tsx → html shell, metadata, ThemeScript - globals.css → base/reset styles + Tailwind import + page.tsx → renders , imports the theme stylesheets + layout.tsx → html shell, metadata, next/font, ThemeScript + globals.css → gruvbox editorial design system (tokens, @utility, base, reveals) + editorial.css → editorial page helpers (social masks, heatmap) + page.css → shared component styles used by the desktop apps + skeuo.css → KDE Plasma desktop styles data.ts → socials, codingProjects, techProjects, friends, wallpapers - page.css → base / terminal theme styles - skeuo.css → Windows/Plasma desktop + mobile home styles revival-inc/page.tsx → fullscreen iframe embedding /public/revival-inc/index.html api/ heatmap/route.ts → proxy for git.dvorakmortonxd.dev contribution heatmap @@ -86,38 +89,44 @@ src/app/ ping/route.ts → service health check (HTTP + minecraft:// via mcstatus.io); host allowlist frame/route.ts → header-stripping proxy so an unframeable friend site loads in a window; host allowlist components/ - ThemeRoot.tsx → picks which shell to show (the router of the UI) - TerminalLayout.tsx → "terminal" theme (Gruvbox) + ThemeRoot.tsx → editorial page by default, desktop when theme === "skeuo" + EditorialSite.tsx → the gruvbox editorial page (nav, hero, sections) + editorial-ui.tsx → editorial primitives: Reveal, Section, ServiceRow, Contributions, … WindowsDesktop.tsx → "skeuo" theme, desktop: icons, windows, taskbar - MobileHome.tsx → "skeuo" theme, phone: app grid + dock WinWindow.tsx → a single draggable/resizable window apps.tsx → APPS registry + the content of each app + in-app terminal + embedded-site windows + the Minecraft-style SMP menu useDesktopIcons.ts → desktop icon grid + drag/drop (THE KEY FILE, see below) - useDragOrder.ts → simple list reordering (mobile home screen only) + useDragOrder.ts → simple list reordering (used inside desktop apps) useTheme.ts → theme state via useSyncExternalStore ThemeScript.tsx → inline pre-hydration script: theme + device detection - ServiceButton.tsx → service link + live status dot (pings /api/ping) - ContributionHeatmap.tsx → heatmap + recent-commit widget (uses the two proxies) + ServiceButton.tsx → desktop service link + live status dot (pings /api/ping) + ContributionHeatmap.tsx → desktop heatmap + recent-commit widget (uses the two proxies) Clock.tsx → live clock (useSyncExternalStore + interval) JokerLogo.tsx → site logo with a click easter egg - SlBackground.tsx → animated ASCII train background (terminal theme) scripts/ dev-server.mjs → custom dev server: live updates minus the dev-only debug surface ``` ### How UI selection works 1. `ThemeScript` runs before paint. It reads `localStorage.theme` and sets - `data-theme` (`skeuo` | `terminal`) and `data-device` (`phone` | `desktop`) - on `` from a user-agent sniff. -2. `useTheme()` (in `useTheme.ts`) reads `data-theme` via - `useSyncExternalStore` and dispatches a `theme-change` event on toggle. -3. `ThemeRoot` shows the terminal when `theme === "terminal"`; otherwise it - mounts **both** `WindowsDesktop` and `MobileHome` and lets CSS - (`html[data-device="phone"]`) decide which is visible. The desktop stays - mounted while hidden so the Welcome window isn't recreated. + `data-theme` (`gruvbox` | `skeuo`) and `data-device` (`phone` | `desktop`) on + `` from a user-agent sniff. On a phone it forces `gruvbox` and CSS hides + every `.desktop-toggle`, so the desktop is unreachable there. +2. `useTheme()` (in `useTheme.ts`) reads `data-theme` via `useSyncExternalStore`; + `setTheme` writes the attribute, persists to `localStorage`, and dispatches + `theme-change`. +3. `ThemeRoot` renders `EditorialSite` unless `theme === "skeuo"`, in which case it + mounts `WindowsDesktop` instead. The editorial page stays mounted (hidden) while + the desktop is up so its scroll position survives; the desktop mounts on demand + (phones can never reach `skeuo`). -Themes: `skeuo` = KDE Plasma / Windows desktop, `terminal` = Gruvbox terminal. +Modes: `gruvbox` = the default editorial site, `skeuo` = KDE Plasma desktop. + +The editorial page is **asset-free** apart from the social SVGs, which are used as +monochrome CSS masks (`.social-mask` in `editorial.css`). `linkedin.svg` is a filled +badge that masks to a plain square, so `editorial-ui.tsx` renders an inline mark for +it instead. The desktop still uses the image assets. --- @@ -180,9 +189,9 @@ Guard (already in place, keep it that way): `e.target` is always the button. ### Related -`useDragOrder.ts` is a **different, simpler** hook used only by `MobileHome` for -reordering the phone app grid/dock (drag one item over another to swap). It is -list reordering, not free placement. Don't confuse the two. +`useDragOrder.ts` is a **different, simpler** hook used inside the desktop apps +for reordering lists (drag one item over another to swap). It is list reordering, +not free placement. Don't confuse the two. --- @@ -216,11 +225,12 @@ list reordering, not free placement. Don't confuse the two. - Components are client components (`"use client"` at the top) — they use pointer events and window APIs freely. -- CSS classes are prefixed by area: `win-` (desktop), `mobile-` (phone), - `app-` / `kickoff` / `terminal` etc. Grep by prefix to find styles. -- Several places deliberately use **imperative DOM work** (drag ghost, - transform-origin for the mobile launch animation). This is intentional for - performance; prefer matching the existing pattern over "Reactifying" it. +- CSS classes are prefixed by area: `win-` (desktop), `app-` / `kickoff` etc. + (desktop windows), plus the editorial design-system classes (`shell`, `display`, + `label`, `rule*`, `glass*`). Grep by prefix to find styles. +- Several places deliberately use **imperative DOM work** (e.g. the drag ghost). + This is intentional for performance; prefer matching the existing pattern over + "Reactifying" it. - `setState` inside an effect is blocked by the ESLint config (`react-hooks/set-state-in-effect`). The one intentional exception is the grid init in `useDesktopIcons.ts`, which carries a scoped diff --git a/package.json b/package.json index 0c02d90..4491ef0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dvorakmortonxd", - "version": "2.1.0", + "version": "3.0.0", "private": true, "scripts": { "dev": "node scripts/dev-server.mjs", diff --git a/src/app/api/ping/route.ts b/src/app/api/ping/route.ts index 0bc19d4..ddcddc2 100644 --- a/src/app/api/ping/route.ts +++ b/src/app/api/ping/route.ts @@ -8,8 +8,8 @@ import { NextResponse } from 'next/server'; // // Host matching is exact or by subdomain (e.g. anything under // dvorakmortonxd.dev). Keep this in sync with the ServiceButton `url`/`checkUrl` -// values in components/apps.tsx and components/TerminalLayout.tsx, and with the -// friend list in data.ts. +// values in components/apps.tsx and the services list in +// components/EditorialSite.tsx, and with the friend list in data.ts. const ALLOWED_HOSTS = [ 'dvorakmortonxd.dev', // ai / jelly / navidrome / git / proxmox / mc (SMP) 'skopec.dev', diff --git a/src/app/components/EditorialSite.tsx b/src/app/components/EditorialSite.tsx new file mode 100644 index 0000000..df660d7 --- /dev/null +++ b/src/app/components/EditorialSite.tsx @@ -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 */} +
+
+ + dvorakmortonxd.dev + I use gentoo btw + + + + +
+ + +
+
+ + {menuOpen ? ( + + ) : null} +
+ + + +
+ {/* ---------------------------------------------------------- hero */} +
+
+ +
+

September 2026  ·  Czech Republic  ·  IT student

+ +

+ I host my own internet, and I write code for fun. +

+ +

+ 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. +

+ + + +

Self-hosted services  ·  retro hardware  ·  pygame game jams

+
+
+ + + + +
+
+ + {/* ----------------------------------------------------------- stats */} +
+
+ {stats.map((s) => ( + + ))} +
+
+ + {/* ----------------------------------------------------------- about */} +
+ + 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:" + /> + + + +
+

+ I’m a sophomore at sssvt.cz + , 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. +

+

+ 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. +

+
+
+
+ + {/* ------------------------------------------------------------- now */} +
+ + What I’m doing right now} + lead="A snapshot, not a feed." + /> + + + +
+

+ 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. +

+

+ Me and my friend Pepa + {" "}are building an education app so we can study more efficiently. It’s called{" "} + + maturita.c + + . +

+
+
+
+ + {/* -------------------------------------------------------- services */} +
+ + 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." + /> + + +
+
    + {SERVICES.map((s) => ( +
  • + +
  • + ))} +
+
+
+ + {/* -------------------------------------------------------- projects */} +
+ + 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." + /> + + +
    + {codingProjects.map((p, i) => { + const { name, status } = splitStatus(p.title); + return ( +
  1. + + {String(i + 1).padStart(2, "0")} + {status ? {status} : null} + +
    + + {name} + +

    {p.desc}

    +
    +
  2. + ); + })} +
+
+ + {/* -------------------------------------------------------- hardware */} +
+ + The hardware pile} + lead="From a 2009 ThinkPad to a 2025 MacBook Air — specs are whatever the machine actually shipped with." + /> + + +
+ {techProjects.map((t) => ( +
+

{t.title}

+

{t.desc}

+
+ ))} +
+
+ + {/* ----------------------------------------------------- contributions */} +
+ + Commits, on my own forge} + lead={ + <> + I run a Forgejo instance at{" "} + + git.dvorakmortonxd.dev + + . It started as a way to stop renting GitHub; it’s public now. + + } + /> + + + +
+ +
+
+
+ + {/* --------------------------------------------------------- friends */} +
+ + People worth visiting} + lead="Sites built by friends — go look at them." + /> + + + +
+ + {/* --------------------------------------------------------- lifting */} +
+ + SBD, five days a week} + lead="A PPL split focused on strength. Current best singles at 73 kg bodyweight:" + /> + + + +
+ {LIFTS.map((l) => ( + + ))} +
+
+
+ + {/* --------------------------------------------------------- contact */} +
+ + Find me elsewhere} + lead={ + <> + Fastest reply is Discord or Instagram. For anything longer, mail{" "} + + dvorak@dvorakmortonxd.dev + + . + + } + /> + + +
+ {socials.map((s) => ( + + + {s.name} + + ))} +
+
+ +
+ + {/* ---------------------------------------------------------- footer */} +
+
+
+
+
dvorakmortonxd.dev
+

+ Built with Next.js. No trackers here — the same reason I run my own services. +

+
+ +
+ +
+ v3.0.0 · © 2025–2026 Martin Dvořák + Follow the GPL-3.0 license (or don’t, I don’t mind). +
+
+
+ + ); +} diff --git a/src/app/components/MobileHome.tsx b/src/app/components/MobileHome.tsx deleted file mode 100644 index f72f167..0000000 --- a/src/app/components/MobileHome.tsx +++ /dev/null @@ -1,232 +0,0 @@ -"use client"; - -import { - useEffect, - useRef, - useState, - useSyncExternalStore, - type CSSProperties, - type MouseEvent, -} from "react"; -import { APPS, APP_BY_ID } from "./apps"; -import Clock from "./Clock"; -import { useDragOrder } from "./useDragOrder"; -import { MOBILE_WALLPAPER } from "../data"; - -const DOCK_IDS = ["projects", "services", "socials", "contributions"]; - -function subscribe(callback: () => void) { - const id = window.setInterval(callback, 1000); - return () => window.clearInterval(id); -} - -/** Digital iOS-style clock widget: big time, small day/date underneath. */ -function HomeWidget() { - const now = useSyncExternalStore( - subscribe, - () => { - const d = new Date(); - return [ - d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }), - d.toLocaleDateString([], { weekday: "long", day: "numeric", month: "short" }), - ].join("\n"); - }, - () => "\n" - ); - const [time, day] = now.split("\n"); - - return ( -
-
- {time} -
-
- {day} -
-
- ); -} - -interface LaunchOrigin { - x: number; - y: number; - scale: number; -} - -export default function MobileHome() { - const [openId, setOpenId] = useState(null); - const [origin, setOrigin] = useState(null); - const [closing, setClosing] = useState(false); - const openApp = openId ? APP_BY_ID[openId] : null; - const closeTimer = useRef(null); - - const [gridApps, setGridApps] = useState(APPS); - const [dockApps, setDockApps] = useState(() => DOCK_IDS.map((id) => ({ id }))); - const gridDrag = useDragOrder(setGridApps); - const dockDrag = useDragOrder(setDockApps); - - const clearCloseTimer = () => { - if (closeTimer.current !== null) { - window.clearTimeout(closeTimer.current); - closeTimer.current = null; - } - }; - - // Fallback for when the close animation never reports completion (e.g. - // reduced-motion, throttled tabs or a missed animationend). Without this the - // full-screen app view can linger and swallow every tap on the home screen. - const closeApp = () => { - setClosing(true); - clearCloseTimer(); - closeTimer.current = window.setTimeout(() => { - setOpenId(null); - setClosing(false); - closeTimer.current = null; - }, 600); - }; - - useEffect(() => () => clearCloseTimer(), []); - - // Compute a transform-origin so the scaled-down app view sits exactly on top - // of the tapped icon, then let it grow to fill the screen (iOS style). - const launch = (appId: string, e: MouseEvent) => { - const rect = e.currentTarget.getBoundingClientRect(); - const icon = rect.width > 0 ? rect.width : rect.height; - // Guard against a zero/degenerate measured icon (e.g. hidden layout) so the - // scale tween never collapses to 0 and the open animation can always run. - const scale = icon > 0 ? Math.min(icon / window.innerWidth, 0.95) : 0.2; - const cx = rect.left + rect.width / 2; - const cy = rect.top + rect.height / 2; - const denom = 1 - scale || 1; - setOrigin({ - x: (cx - (scale * window.innerWidth) / 2) / denom, - y: (cy - (scale * window.innerHeight) / 2) / denom, - scale, - }); - setClosing(false); - setOpenId(appId); - }; - - const renderIcon = (appId: string, inDock: boolean, drag: ReturnType) => { - const app = APP_BY_ID[appId]; - if (!app) return null; - return ( - - ); - }; - - // The transform origin is exposed as a CSS variable (not an inline - // transform-origin) so only the opening keyframe uses the tapped icon as its - // pivot. The closing animation keeps a centred origin and always slides down. - const viewStyle = origin - ? ({ - "--app-origin": `${origin.x}px ${origin.y}px`, - "--app-scale": String(origin.scale), - } as CSSProperties) - : undefined; - - return ( -
-
-
- - - - -
- -
- - {gridApps.map((app) => renderIcon(app.id, false, gridDrag))} -
- -
- {dockApps.map((d) => renderIcon(d.id, true, dockDrag))} -
- - -
- - {openApp && ( -
{ - if (e.target !== e.currentTarget) return; - if (closing) { - clearCloseTimer(); - setOpenId(null); - setClosing(false); - } - }} - > -
- - {openApp.title} -
-
{openApp.content}
-
- )} -
- ); -} diff --git a/src/app/components/SlBackground.tsx b/src/app/components/SlBackground.tsx deleted file mode 100644 index 4f9ad36..0000000 --- a/src/app/components/SlBackground.tsx +++ /dev/null @@ -1,106 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; - -const TRAIN_ASCII = ` ==== ________ ___________ - _D _| |_______/ \\__I_I_____===__|_________| - |(_)--- | H\\________/ | | =|___ ___| _________________ _________________ _________________ _________________ _________________ _________________ - / | | H | | | | ||_| |_|| _| \\_____| \\_____| \\_____| \\_____| \\_____| \\_____A - | | | H |__--------------------| [___] | =| | | | | | | - | ________|___H__/__|_____/[][]~\\_______| | -| | | | | | | - |/ | |-----------I_____I [][] [] D |=======|____|________________________|_______________________|_______________________|_______________________|_______________________|_______________________|_ -__/ =| o |=-~~\\ /~~\\ /~~\\ /~~\\ ____Y___________|__|__________________________|_______________________|_______________________|_______________________|_______________________|_______________________| - |/-=|___|= || || || |_____/~\\___/ |_D__D__D_| |_D__D__D_| |_D__D__D_| |_D__D__D_| |_D__D__D_| |_D__D__D_| |_D__D__D_| |_D__D__D_| |_D__D__D_| |_D__D__D_| |_D__D__D_| |_D__D__D_| - \\_/ \\O=====O=====O=====O_/ \\_/ \\_/ \\_/ \\_/ \\_/ \\_/ \\_/ \\_/ \\_/ \\_/ \\_/ \\_/ \\_/ \\_/ \\_/ \\_/ \\_/ \\_/ \\_/ \\_/ \\_/ \\_/ \\_/ \\_/ \\_/`; - -export default function SlBackground() { - const [pos, setPos] = useState(150); - - useEffect(() => { - let animationFrameId: number; - let lastTime = 0; - - const animate = (timestamp: number) => { - if (!lastTime) lastTime = timestamp; - const delta = timestamp - lastTime; - - // Update position every ~30ms to create a retro sliding terminal refresh feel - if (delta > 45) { - setPos((prev) => { - if (prev <= -150) return 150; - return prev - 1.5; - }); - lastTime = timestamp; - } - animationFrameId = requestAnimationFrame(animate); - }; - - animationFrameId = requestAnimationFrame(animate); - return () => cancelAnimationFrame(animationFrameId); - }, []); - - return ( - <> - -
-
-          {TRAIN_ASCII}
-        
-
- - ); -} - - - - - - - - - - diff --git a/src/app/components/TerminalLayout.tsx b/src/app/components/TerminalLayout.tsx deleted file mode 100644 index e48b482..0000000 --- a/src/app/components/TerminalLayout.tsx +++ /dev/null @@ -1,603 +0,0 @@ -import Image from "next/image"; -import type { CSSProperties } from "react"; -import SlBackground from "./SlBackground"; -import JokerLogo from "./JokerLogo"; -import ServiceButton from "./ServiceButton"; -import ContributionHeatmap from "./ContributionHeatmap"; -import { useTheme } from "./useTheme"; -import { socials, codingProjects, techProjects, friends } from "../data"; - -type PanelStyle = CSSProperties & { "--panel-color"?: string }; - -const panelStyle = (color: string, style: CSSProperties = {}): PanelStyle => ({ - ...style, - "--panel-color": color, -}); - -export default function TerminalLayout() { - const { toggle } = useTheme(); - - return ( - <> - -
-
-
- -

dvorakmortonxd.dev

- -
-
- -
-
- - README.MD - -

$ whoami

-

- Welcome my personal website! Here I show off my projects and hardware I own, - aswell as my self-hosted services :) -

-
- -
-
- - SOCIALS.JS - -
- {socials.map((s) => ( - - {s.name} - {s.name} - - ))} -
-
- mail if u ever wanna contact me:{" "} - dvorak@dvorakmortonxd.dev -
-
-
- -
-
- - GYM_SBD_AT_73KG_BW.DB - -
-
- Squat -
SQUAT
-
125kg
-
5.1.2025
-
-
- Bench -
BENCH
-
111kg
-
29.5.2026
-
-
- Deadlift -
DEADLIFT
-
165kg
-
17.12.2024
-
-
-
-
- -
-
- - NOW.TXT - -
-

- martin@dvorakmortonxd: - ~$ cat now.txt -

-
-

- I'm currently hosting an SMP (dm me on discord). Also I'm pretty - much cooking daily, because im bulking through out the non-summer seasons. - And me and my pal Pepa are working on an - education app so we can study more efficiently, It's called{" "} - maturita.c! -

-
-
-
-
- -
-
- - README.MD - -
-

- martin@dvorakmortonxd: - ~$ cat about_me.md -

-
-

- Hey, I'm Martin Dvořák — a 17(soon to be 18)-year-old IT student based - in the Czech Republic, specializing in network management and - administration at sssvt.cz. I'm a - sophomore and I have wanted to go to this school since 7th grade (2022) so - I'm happy I got in in 2025, the school is pretty cool so if you are in - middle school and interested in IT, make sure to check it out. I'm a - homelabber and developer heavily focused on self-hosting{" "} - literally everything, writing code (for example games for gamejams), - tinkering with GNU/Linux or Musl/Linux systems and retro hardware. I own a - small collection of laptops ranging from 2009 to 2014 (and ofcourse my 2025 - macbook), most of these found in ewaste. In my free time I mostly spend - time with my gf {"<"}3, I also spend a bit of time setting up my self - hosted stuff (like jellyfin or gitdxd), and also when I'm in my gaming - mood I play terraria (around 1300hr playtime btw) and minecraft modpacks - with mekanism and/or create. I also go to the gym around 3-5 times a week, - running a PPL split focused on strength/powerlifting and especially SBD. I - take a big focus on my health and physique. - -

-
-
-
-
- -
-
- - SERVICES.HTML - -
- - - - - - -
-
-
- -
-
- - GITDXD.GIT - -
-

- GitDXD -

-

- My Self-hosted Git Instance -

-
- GitDXD Preview -
-
-

- My very own forgejo git instance, originally started as a private git - instance for me and my friends to boycott GitHub but I've made it - public now so. Join at{" "} - - git.dvorakmortonxd.dev - - . -

-
-
-
-
- -
-
- - CONTRIBUTIONS@GITDXD.GIT - -
- -
-
-
- -
-
- - REVIVAL.INC - -
-

- Revival inc. -

-
- revival inc thumbnail -
-

- What is it? -

-
-

- It's a game I made for the game jam "Mini Jame Gam #55", the - theme was Necromancy and the special item was Milk, the game also includes - a tutorial so if you wanna try it do it. Check it out at{" "} - - itch.io - {" "} - or play{" "} - - here - - . -

-
-
-
-
- -
-
- - RUSTYWOLF.RS - -
-

- Introducing RustyWolf -

-
- RustyWolf Browser Preview -
-

- What makes it so cool??? -

-
-

- A lightweight browser made in Rust with WebKitGTK. It's the successor - to my previous project Venturesome, aiming to provide a fast and minimalist - browsing experience. It's a perfect browser for old machines that - struggle with modern browsers. Check it out at{" "} - - - GitDXD - - - . -

-
-
-
-
-
- -
-
-
- - TECH_LIST.JSON - -

- All my hardware equipment :) -

-
- {techProjects.map((project) => ( -
-
-

{project.title}

-

{project.desc}

-
- ))} -
-
-
- -
-
- - PROJECTS.DOCX - -

- All the stuff I've worked on :)) -

-
- {codingProjects.map((project, idx) => ( - - -
-
- -
-
- - FRIENDS.TSX - -

- Friend's websites and projects -

-
- {friends.map((friend, idx) => ( - - } - name={friend.name} - desc={friend.desc} - /> - ))} -
-
-
-
- -
- v2.1.0 - © 2025-2026 Martin Dvořák. All rights reserved (not really I dont care, just follow - the GPL3.0 License) -
-
- - ); -} diff --git a/src/app/components/ThemeRoot.tsx b/src/app/components/ThemeRoot.tsx index 8b49383..9366806 100644 --- a/src/app/components/ThemeRoot.tsx +++ b/src/app/components/ThemeRoot.tsx @@ -1,32 +1,25 @@ "use client"; import { useTheme } from "./useTheme"; -import TerminalLayout from "./TerminalLayout"; +import EditorialSite from "./EditorialSite"; import WindowsDesktop from "./WindowsDesktop"; -import MobileHome from "./MobileHome"; +// The gruvbox editorial site is the default. Phones can never reach the KDE +// desktop: ThemeScript forces gruvbox there (ignoring a stored "skeuo") and CSS +// hides the Desktop-mode buttons, so `theme === "skeuo"` is desktop-only. +// +// The editorial page stays mounted (hidden) while the desktop is up, so its +// scroll position survives a round-trip. The desktop mounts on demand. export default function ThemeRoot() { const { theme } = useTheme(); - const terminal = theme === "terminal"; + const desktop = theme === "skeuo"; - // The desktop is kept mounted (just hidden) while the Gruvbox/terminal theme - // is active. That way the Welcome window is only created on a fresh page - // load and isn't reopened by toggling themes. return ( <> -
- +
+
-
- -
- {terminal && } + {desktop ? : null} ); } diff --git a/src/app/components/ThemeScript.tsx b/src/app/components/ThemeScript.tsx index 2546edf..8779772 100644 --- a/src/app/components/ThemeScript.tsx +++ b/src/app/components/ThemeScript.tsx @@ -7,7 +7,10 @@ import { useServerInsertedHTML } from "next/navigation"; // without a flash. It is injected with useServerInsertedHTML so the