ui update!

This commit is contained in:
dvorakmortonxd 2026-09-22 11:41:05 +02:00
commit d2f99c87b9
16 changed files with 3085 additions and 592 deletions

View file

@ -0,0 +1,43 @@
"use client";
import { useEffect, useState } from "react";
function format(date: Date) {
return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
}
export default function Clock({
className,
withSeconds = false,
}: {
className?: string;
withSeconds?: boolean;
}) {
const [now, setNow] = useState<Date | null>(() =>
typeof window === "undefined" ? null : new Date()
);
useEffect(() => {
const id = window.setInterval(
() => setNow(new Date()),
withSeconds ? 1000 : 10000
);
return () => window.clearInterval(id);
}, [withSeconds]);
const text = now
? withSeconds
? now.toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
})
: format(now)
: "";
return (
<span className={className} suppressHydrationWarning>
{text}
</span>
);
}

View file

@ -0,0 +1,77 @@
"use client";
import { useState } from "react";
import { APPS, APP_BY_ID } from "./apps";
import Clock from "./Clock";
import { useTheme } from "./useTheme";
import { WALLPAPER } from "../data";
export default function IosHome() {
const { theme, toggle } = useTheme();
const [openId, setOpenId] = useState<string | null>(null);
const openApp = openId ? APP_BY_ID[openId] : null;
return (
<div className="ios-root">
{/* iOS app view */}
{openApp ? (
<div className="ios-app-view">
<div className="ios-navbar">
<button type="button" className="ios-back" onClick={() => setOpenId(null)}>
‹ Home
</button>
<span className="ios-navbar-title">{openApp.title}</span>
<span className="ios-navbar-spacer" />
</div>
<div className="ios-app-body app-surface">{openApp.content}</div>
</div>
) : (
<div
className="ios-home"
style={{ backgroundImage: `url('${WALLPAPER}')` }}
>
<div className="ios-statusbar">
<span className="ios-status-time">
<Clock />
</span>
<span className="ios-status-icons" aria-hidden="true">
●●● ᯤ ▮
</span>
</div>
<button
type="button"
className="ios-theme-app"
onClick={toggle}
aria-label={`Switch to ${theme === "skeuo" ? "terminal" : "Aero"} theme`}
>
<span className="ios-theme-app-icon" aria-hidden="true">
{theme === "skeuo" ? "▚" : "❖"}
</span>
<span className="ios-theme-app-label">{theme === "skeuo" ? "Terminal" : "Aero"}</span>
</button>
<div className="ios-grid">
{APPS.map((app) => (
<button
key={app.id}
type="button"
className="ios-app"
onClick={() => setOpenId(app.id)}
>
<span className="ios-app-icon" style={{ background: app.color }}>
{app.icon}
</span>
<span className="ios-app-label">{app.title}</span>
</button>
))}
</div>
<div className="ios-dock">
<span className="ios-dock-slots" aria-hidden="true" />
</div>
</div>
)}
</div>
);
}

View file

@ -51,6 +51,7 @@ export default function SlBackground() {
`}
</style>
<div
className="sl-background"
style={{
position: "fixed",
top: 0,

View file

@ -0,0 +1,726 @@
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 Clock from "./Clock";
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 { theme, toggle } = useTheme();
return (
<>
<SlBackground />
<div className="container">
<header className="header">
<div className="header-center">
<JokerLogo />
<h1 className="site-name">dvorakmortonxd.dev</h1>
</div>
</header>
<main className="hero">
<div className="hero-panel" style={panelStyle("var(--accent)")}>
<span
className="hero-panel-label"
style={{ color: "var(--accent)", borderColor: "var(--accent)" }}
>
README.MD
</span>
<h2 className="hero-title">$ whoami</h2>
<p className="hero-subtitle">
Welcome my personal website! Here I show off my projects and hardware I own,
aswell as my self-hosted services :)<span className="cursor-blink"></span>
</p>
</div>
<div className="search-section">
<div className="tui-panel" style={panelStyle("var(--white)")}>
<span
className="tui-panel-label"
style={{ color: "var(--white)", borderColor: "var(--white)" }}
>
SEARXNG.C
</span>
<form
action="https://search.dvorakmortonxd.dev/search"
method="GET"
target="_blank"
rel="noopener noreferrer"
className="search-form"
>
<input
type="text"
name="q"
placeholder="Search... (only works for those who are whitelisted)"
className="search-bar"
/>
</form>
</div>
</div>
<div className="social-section">
<div className="tui-panel" style={panelStyle("var(--accent)")}>
<span
className="tui-panel-label"
style={{ color: "var(--accent)", borderColor: "var(--accent)" }}
>
SOCIALS.JS
</span>
<div className="social-buttons">
{socials.map((s) => (
<a
key={s.name}
href={s.url}
target="_blank"
rel="noopener noreferrer"
className="social-button"
>
<Image
src={s.icon}
alt={s.name}
width={16}
height={16}
className="social-icon"
/>
<span className="social-label">{s.name}</span>
</a>
))}
</div>
<div className="mail-contact">
mail if u ever wanna contact me:{" "}
<a href="mailto:dvorak@dvorakmortonxd.dev">dvorak@dvorakmortonxd.dev</a>
</div>
</div>
</div>
<div className="gym-section">
<div className="tui-panel" style={panelStyle("var(--white)")}>
<span
className="tui-panel-label"
style={{ color: "var(--white)", borderColor: "var(--white)" }}
>
GYM_SBD_AT_73KG_BW.DB
</span>
<div className="gym-grid">
<div className="gym-item">
<Image src="/squat.svg" alt="Squat" width={48} height={48} className="gym-icon" />
<div className="gym-label">SQUAT</div>
<div className="gym-weight">125kg</div>
<div className="gym-date">5.1.2025</div>
</div>
<div className="gym-item">
<Image src="/bench.svg" alt="Bench" width={48} height={48} className="gym-icon" />
<div className="gym-label">BENCH</div>
<div className="gym-weight">111kg</div>
<div className="gym-date">29.5.2026</div>
</div>
<div className="gym-item">
<Image
src="/deadlift.svg"
alt="Deadlift"
width={48}
height={48}
className="gym-icon"
/>
<div className="gym-label">DEADLIFT</div>
<div className="gym-weight">165kg</div>
<div className="gym-date">17.12.2024</div>
</div>
</div>
</div>
</div>
<div className="about-section">
<div className="tui-panel" style={panelStyle("var(--white)")}>
<span
className="tui-panel-label"
style={{ color: "var(--white)", borderColor: "var(--white)" }}
>
NOW.TXT
</span>
<div className="about-content">
<p>
<span style={{ color: "var(--green)" }}>martin@dvorakmortonxd</span>:
<span className="text-blue">~</span>$ cat now.txt
</p>
<div style={{ color: "#a3a3a3" }}>
<p>
I&#39;m currently hosting an SMP (dm me on discord). Also I&#39;m pretty
much cooking daily, because im bulking through out the non-summer seasons.
And me and my pal <a href="https://skopec.dev">Pepa</a> are working on an
education app so we can study more efficiently, It&#39;s called{" "}
<a href="https://git.dvorakmortonxd.dev/edu/maturita.c">maturita.c</a>!
</p>
</div>
</div>
</div>
</div>
<div className="about-section">
<div className="tui-panel" style={panelStyle("var(--accent)")}>
<span
className="tui-panel-label"
style={{ color: "var(--accent)", borderColor: "var(--accent)" }}
>
README.MD
</span>
<div className="about-content">
<p>
<span className="text-green">martin@dvorakmortonxd</span>:
<span className="text-blue">~</span>$ cat about_me.md
</p>
<div style={{ color: "#a3a3a3" }}>
<p>
Hey, I&#39;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 <a href="https://sssvt.cz">sssvt.cz</a>. I&#39;m a
sophomore and I have wanted to go to this school since 7th grade (2022) so
I&#39;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&#39;m a
homelabber and developer heavily focused on self-hosting{" "}
<i>literally everything</i>, 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&#39;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.
<span className="cursor-blink"></span>
</p>
</div>
</div>
</div>
</div>
<div className="services-section">
<div className="tui-panel" style={panelStyle("var(--accent)")}>
<span
className="tui-panel-label"
style={{ color: "var(--accent)", borderColor: "var(--accent)" }}
>
SERVICES.HTML
</span>
<div className="services-buttons services-main">
<ServiceButton
url="https://proxmox.dvorakmortonxd.dev/"
className="proxmox"
icon="🖥️"
name="Proxmox"
desc="Server Management"
access="private"
disabled
/>
<ServiceButton
url="https://ai.dvorakmortonxd.dev/"
className="ai"
icon="🦾"
name="Llama.cpp"
desc="Local AI Server"
access="on_request"
/>
<ServiceButton
url="https://jelly.dvorakmortonxd.dev/"
className="jellyfin"
icon="🎬"
name="Jellyfin"
desc="Local Media Server"
access="on_request"
/>
<ServiceButton
url="https://discord.gg/vyvhk7d8bX"
checkUrl="minecraft://mc.dvorakmortonxd.dev:25565"
className="pterodactyl"
icon="🎮"
name="SMP"
desc="My Survival Minecraft Server"
access="on_request"
/>
<ServiceButton
url="https://navidrome.dvorakmortonxd.dev/"
className="funkwhale"
icon="🎸"
name="Navidrome"
desc="Music Streaming"
access="private"
/>
<ServiceButton
url="https://git.dvorakmortonxd.dev"
className="gitdxd"
icon="🐙"
name="GitDXD"
desc="Best Git Hosting OAT"
access="public"
/>
</div>
</div>
</div>
<div className="gitdxd-section" style={{ marginTop: "20px" }}>
<div className="tui-panel" style={panelStyle("var(--white)")}>
<span
className="tui-panel-label"
style={{ color: "var(--white)", borderColor: "var(--white)" }}
>
GITDXD.GIT
</span>
<div style={{ padding: "24px 16px 16px" }}>
<h2
style={{
fontSize: "2.4rem",
fontWeight: "bold",
color: "var(--white)",
marginBottom: "16px",
textAlign: "center",
}}
>
GitDXD
</h2>
<p
style={{
fontSize: "1.8rem",
color: "var(--white)",
fontWeight: "500",
textAlign: "center",
}}
>
My Self-hosted Git Instance
</p>
<div
style={{
marginTop: "16px",
marginBottom: "16px",
borderRadius: "8px",
overflow: "hidden",
border: "1px solid rgba(255, 255, 255, 0.4)",
boxShadow: "0 0 30px rgba(180, 255, 125, 0.15)",
}}
>
<Image
src="/gitdxd.webp"
alt="GitDXD Preview"
width={1280}
height={720}
style={{ width: "100%", height: "auto", display: "block" }}
/>
</div>
<div
style={{
color: "var(--gray)",
marginTop: "12px",
lineHeight: "1.6",
textAlign: "center",
}}
>
<p>
My very own forgejo git instance, originally started as a private git
instance for me and my friends to boycott GitHub but I&#39;ve made it
public now so. Join at{" "}
<span className="text-purple-500">
<a href="https://git.dvorakmortonxd.dev">git.dvorakmortonxd.dev</a>
</span>
.<span className="cursor-blink"></span>
</p>
</div>
</div>
</div>
</div>
<div className="commits-section" style={{ marginTop: "20px" }}>
<div className="tui-panel" style={panelStyle("var(--accent)")}>
<span
className="tui-panel-label"
style={{ color: "var(--accent)", borderColor: "var(--accent)" }}
>
CONTRIBUTIONS@GITDXD.GIT
</span>
<div className="commits-content" style={{ padding: "24px 12px 16px" }}>
<ContributionHeatmap />
</div>
</div>
</div>
<div className="revival-section" style={{ marginTop: "20px" }}>
<div className="tui-panel" style={panelStyle("var(--white)")}>
<span
className="tui-panel-label"
style={{ color: "var(--white)", borderColor: "var(--white)" }}
>
REVIVAL.INC
</span>
<div style={{ padding: "24px 16px 16px" }}>
<h2
style={{
fontSize: "2.4rem",
fontWeight: "bold",
color: "var(--white)",
marginBottom: "32px",
textAlign: "center",
}}
>
Revival inc.
</h2>
<div
style={{
marginTop: "12px",
marginBottom: "12px",
borderRadius: "8px",
overflow: "hidden",
border: "1px solid rgba(0, 0, 0, 0.4)",
boxShadow: "0 0 30px rgba(255, 255, 255, 0.35)",
width: "80%",
margin: "12px auto",
}}
>
<Image
src="/revivalinc.webp"
alt="revival inc thumbnail"
width={768}
height={576}
style={{ width: "100%", height: "auto", display: "block" }}
/>
</div>
<p
style={{
fontSize: "1.8rem",
color: "var(--white)",
fontWeight: "500",
textAlign: "center",
}}
>
What is it?
</p>
<div style={{ color: "var(--gray)", marginTop: "12px", lineHeight: "1.6" }}>
<p>
It&#39;s a game I made for the game jam &#34;Mini Jame Gam #55&#34;, 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{" "}
<span className="text-purple-500">
<a href="https://martin-dv.itch.io/revival-inc">itch.io</a>
</span>{" "}
or play{" "}
<span className="text-purple-500">
<a href="https://dvorakmortonxd.dev/revival-inc">here</a>
</span>
.<span className="cursor-blink"></span>
</p>
</div>
</div>
</div>
</div>
<div className="rustywolf-section" style={{ marginTop: "20px" }}>
<div className="tui-panel" style={panelStyle("var(--white)")}>
<span
className="tui-panel-label"
style={{ color: "var(--white)", borderColor: "var(--white)" }}
>
RUSTYWOLF.RS
</span>
<div style={{ padding: "24px 16px 16px" }}>
<h2
style={{
fontSize: "2.4rem",
fontWeight: "bold",
color: "var(--white)",
marginBottom: "32px",
textAlign: "center",
}}
>
Introducing RustyWolf
</h2>
<div
style={{
marginTop: "12px",
marginBottom: "12px",
borderRadius: "8px",
overflow: "hidden",
border: "1px solid rgba(255, 255, 255, 0.4)",
boxShadow: "0 0 30px rgba(255, 255, 255, 0.2)",
}}
>
<Image
src="/RustyWolfPreview.webp"
alt="RustyWolf Browser Preview"
width={1280}
height={720}
style={{ width: "100%", height: "auto", display: "block" }}
/>
</div>
<p
style={{
fontSize: "1.8rem",
color: "var(--white)",
fontWeight: "500",
textAlign: "center",
}}
>
What makes it so cool???
</p>
<div style={{ color: "var(--gray)", marginTop: "12px", lineHeight: "1.6" }}>
<p>
A lightweight browser made in Rust with WebKitGTK. It&#39;s the successor
to my previous project Venturesome, aiming to provide a fast and minimalist
browsing experience. It&#39;s a perfect browser for old machines that
struggle with modern browsers. Check it out at{" "}
<span style={{ color: "var(--accent2)" }}>
<a href="https://git.dvorakmortonxd.dev/dvorakmortonxd/RustyWolf">
GitDXD
</a>
</span>
.<span className="cursor-blink"></span>
</p>
</div>
</div>
</div>
</div>
</main>
<section className="projects">
<div className="subsection-wrapper">
<div className="subsection-panel" style={panelStyle("var(--white)")}>
<span
className="subsection-panel-label"
style={{ color: "var(--white)", borderColor: "var(--white)" }}
>
TECH_LIST.JSON
</span>
<h2
style={{
fontSize: "2rem",
fontWeight: "bold",
textAlign: "center",
margin: "4px 0 20px",
color: "var(--white)",
}}
>
All my hardware equipment :)
</h2>
<div className="project-grid">
{techProjects.map((project) => (
<div key={project.title} className="project-card">
<div
className="project-image"
style={{ backgroundImage: `url(${project.image})` }}
/>
<h4 className="project-title">{project.title}</h4>
<p className="project-desc">{project.desc}</p>
</div>
))}
</div>
</div>
</div>
<div className="subsection-wrapper">
<div className="subsection-panel coding-panel" style={panelStyle("var(--accent)")}>
<span
className="subsection-panel-label"
style={{ color: "var(--accent)", borderColor: "var(--accent)" }}
>
PROJECTS.DOCX
</span>
<h2
style={{
fontSize: "2rem",
fontWeight: "bold",
textAlign: "center",
margin: "4px 0 20px",
color: "var(--accent)",
}}
>
All the stuff I&#39;ve worked on :))
</h2>
<div className="project-grid">
{codingProjects.map((project, idx) => (
<a
key={idx}
href={project.url}
target="_blank"
rel="noopener noreferrer"
className="project-card"
>
<div
className="project-image"
style={{ backgroundImage: `url(${project.img})` }}
/>
<h4 className="project-title">{project.title}</h4>
<p className="project-desc">{project.desc}</p>
</a>
))}
</div>
</div>
</div>
<div className="subsection-wrapper">
<div
className="subsection-panel friends-panel"
style={panelStyle("var(--white)", { backgroundColor: "rgba(255, 255, 255, 0.02)" })}
>
<span
className="subsection-panel-label"
style={{ color: "var(--white)", borderColor: "var(--white)" }}
>
FRIENDS.TSX
</span>
<h2
style={{
fontSize: "2rem",
fontWeight: "bold",
textAlign: "center",
margin: "4px 0 20px",
color: "var(--white)",
}}
>
Friend&#39;s websites and projects
</h2>
<div className="services-buttons" style={{ gap: "1rem" }}>
{friends.map((friend, idx) => (
<ServiceButton
key={idx}
url={friend.realUrl || friend.url}
className="friend"
icon={
<Image
src={friend.img}
alt={friend.name}
width={68}
height={68}
className="friend-icon"
style={{
borderRadius: "50%",
filter:
"brightness(0) invert(65%) hue-rotate(70deg) saturate(100%)",
}}
/>
}
name={friend.name}
desc={friend.desc}
/>
))}
</div>
<div
style={{
marginTop: "12px",
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "16px",
}}
>
<h3 style={{ fontSize: "1.5rem", color: "var(--accent)", fontWeight: "600" }}>
Quipee by Bukovina Filip &amp; Vlk Tomáš
</h3>
<div
style={{
width: "100%",
borderRadius: "8px",
overflow: "hidden",
border: "1px solid rgba(255, 255, 0, 0.3)",
boxShadow: "0 0 20px rgba(255, 255, 0, 0.1)",
}}
>
<Image
src="/quipee.webp"
alt="Quipee Thumbnail"
width={1280}
height={720}
style={{ width: "100%", height: "auto", display: "block" }}
/>
</div>
<div
style={{
display: "flex",
gap: "12px",
flexWrap: "wrap",
justifyContent: "center",
}}
>
<a
href="https://quipee.me"
target="_blank"
rel="noopener noreferrer"
style={{
backgroundColor: "rgba(255, 255, 255, 0.04)",
color: "var(--white)",
padding: "6px 16px",
borderRadius: "6px",
fontSize: "1rem",
fontWeight: "500",
textDecoration: "none",
border: "1px solid rgba(255, 255, 0, 0.3)",
}}
>
Web
</a>
<a
href="https://drive.google.com/drive/folders/1oi76JyWs6J84MUmCGV7GvtLUexXI8Jel?usp=share_link"
target="_blank"
rel="noopener noreferrer"
style={{
backgroundColor: "rgba(255, 255, 255, 0.04)",
color: "var(--white)",
padding: "6px 16px",
borderRadius: "6px",
fontSize: "1rem",
fontWeight: "500",
textDecoration: "none",
border: "1px solid rgba(255, 255, 0, 0.3)",
}}
>
Android
</a>
<a
href="https://testflight.apple.com/join/hP2a1E5x"
target="_blank"
rel="noopener noreferrer"
style={{
backgroundColor: "rgba(255, 255, 255, 0.04)",
color: "var(--white)",
padding: "6px 16px",
borderRadius: "6px",
fontSize: "1rem",
fontWeight: "500",
textDecoration: "none",
border: "1px solid rgba(255, 255, 0, 0.3)",
}}
>
IOS
</a>
</div>
</div>
</div>
</div>
</section>
<footer className="footer">
<span className="footer-label">v1.2.1</span>
© 2025-2026 Martin Dvořák. All rights reserved (not really I dont care, just follow
the GPL3.0 License)
</footer>
</div>
<div className="tui-statusbar">
<span className="tui-statusbar-item">theme: terminal</span>
<button type="button" className="tui-statusbar-btn" onClick={toggle}>
[ switch to {theme === "skeuo" ? "terminal" : "Aero"} ]
</button>
<span className="tui-statusbar-spacer" />
<Clock className="tui-statusbar-clock" />
</div>
</>
);
}

View file

@ -0,0 +1,25 @@
"use client";
import { useTheme } from "./useTheme";
import TerminalLayout from "./TerminalLayout";
import WindowsDesktop from "./WindowsDesktop";
import IosHome from "./IosHome";
export default function ThemeRoot() {
const { theme } = useTheme();
if (theme === "terminal") {
return <TerminalLayout />;
}
return (
<>
<div className="skeuo-desktop-root">
<WindowsDesktop />
</div>
<div className="skeuo-ios-root">
<IosHome />
</div>
</>
);
}

View file

@ -0,0 +1,109 @@
"use client";
import { useRef, type CSSProperties } from "react";
import type { AppDef } from "./apps";
export interface WinState {
id: number;
appId: string;
x: number;
y: number;
w: number;
h: number;
z: number;
minimized: boolean;
maximized: boolean;
}
interface Props {
win: WinState;
app: AppDef;
active: boolean;
onFocus: () => void;
onClose: () => void;
onMinimize: () => void;
onToggleMaximize: () => void;
onMove: (x: number, y: number) => void;
}
export default function WinWindow({
win,
app,
active,
onFocus,
onClose,
onMinimize,
onToggleMaximize,
onMove,
}: Props) {
const drag = useRef<{ sx: number; sy: number; ox: number; oy: number } | null>(null);
const style: CSSProperties = win.maximized
? { left: 0, top: 0, width: "100%", height: "100%", zIndex: win.z }
: { left: win.x, top: win.y, width: win.w, height: win.h, zIndex: win.z };
return (
<section
className={
"win-window" +
(active ? " active" : "") +
(win.minimized ? " minimized" : "") +
(win.maximized ? " maximized" : "")
}
style={style}
onPointerDown={onFocus}
role="dialog"
aria-label={app.title}
>
<div
className="win-titlebar"
onPointerDown={(e) => {
onFocus();
if (win.maximized) return;
if ((e.target as HTMLElement).closest(".win-controls")) return;
drag.current = { sx: e.clientX, sy: e.clientY, ox: win.x, oy: win.y };
e.currentTarget.setPointerCapture(e.pointerId);
}}
onPointerMove={(e) => {
const d = drag.current;
if (!d) return;
onMove(d.ox + (e.clientX - d.sx), d.oy + (e.clientY - d.sy));
}}
onPointerUp={(e) => {
drag.current = null;
try {
e.currentTarget.releasePointerCapture(e.pointerId);
} catch {
/* ignore */
}
}}
>
<span className="win-titlebar-icon" style={{ background: app.color }} aria-hidden="true">
{app.icon}
</span>
<span className="win-titlebar-text">{app.title}</span>
<div className="win-controls">
<button
type="button"
className="win-ctl win-ctl-min"
onClick={onMinimize}
aria-label="Minimize"
/>
<button
type="button"
className="win-ctl win-ctl-max"
onClick={onToggleMaximize}
aria-label={win.maximized ? "Restore" : "Maximize"}
/>
<button
type="button"
className="win-ctl win-ctl-close"
onClick={onClose}
aria-label="Close"
/>
</div>
</div>
<div className="win-body app-surface">{app.content}</div>
</section>
);
}

View file

@ -0,0 +1,274 @@
"use client";
import { useRef, useState } from "react";
import { APPS, APP_BY_ID } from "./apps";
import WinWindow, { type WinState } from "./WinWindow";
import Clock from "./Clock";
import { useTheme } from "./useTheme";
import { WALLPAPER } from "../data";
const TASKBAR_HEIGHT = 40;
export default function WindowsDesktop() {
const { theme, toggle } = useTheme();
const [wins, setWins] = useState<WinState[]>(() => [
{
id: 1,
appId: "welcome",
x: 110,
y: 64,
w: 620,
h: 430,
z: 10,
minimized: false,
maximized: false,
},
]);
const [activeId, setActiveId] = useState<number | null>(1);
const [startOpen, setStartOpen] = useState(false);
const [selectedIcon, setSelectedIcon] = useState<string | null>(null);
const idRef = useRef(2);
const zRef = useRef(11);
const spawnRef = useRef(1);
const focus = (id: number) => {
setActiveId(id);
setWins((prev) =>
prev.map((w) => (w.id === id ? { ...w, z: ++zRef.current } : w))
);
};
const openApp = (appId: string) => {
setStartOpen(false);
const existing = wins.find((w) => w.appId === appId);
if (existing) {
setWins((prev) =>
prev.map((w) =>
w.id === existing.id ? { ...w, minimized: false, z: ++zRef.current } : w
)
);
setActiveId(existing.id);
return;
}
const app = APP_BY_ID[appId];
if (!app) return;
const n = spawnRef.current++;
const id = idRef.current++;
const maxX = Math.max(20, (typeof window !== "undefined" ? window.innerWidth : 1280) - app.width - 20);
const maxY = Math.max(20, (typeof window !== "undefined" ? window.innerHeight : 800) - TASKBAR_HEIGHT - app.height - 20);
const x = Math.min(maxX, 90 + (n % 8) * 30);
const y = Math.min(maxY, 56 + (n % 8) * 28);
setWins((prev) => [
...prev,
{
id,
appId,
x,
y,
w: app.width,
h: app.height,
z: ++zRef.current,
minimized: false,
maximized: false,
},
]);
setActiveId(id);
};
const closeWin = (id: number) =>
setWins((prev) => prev.filter((w) => w.id !== id));
const minimizeWin = (id: number) =>
setWins((prev) => prev.map((w) => (w.id === id ? { ...w, minimized: true } : w)));
const toggleMax = (id: number) =>
setWins((prev) =>
prev.map((w) => (w.id === id ? { ...w, maximized: !w.maximized } : w))
);
const moveWin = (id: number, x: number, y: number) => {
const maxX =
(typeof window !== "undefined" ? window.innerWidth : 1280) - 120;
const maxY =
(typeof window !== "undefined" ? window.innerHeight : 800) - TASKBAR_HEIGHT - 20;
setWins((prev) =>
prev.map((w) =>
w.id === id
? { ...w, x: Math.max(-w.w + 120, Math.min(maxX, x)), y: Math.max(0, Math.min(maxY, y)) }
: w
)
);
};
const taskbarClick = (w: WinState) => {
if (w.minimized) {
setWins((prev) =>
prev.map((x) => (x.id === w.id ? { ...x, minimized: false, z: ++zRef.current } : x))
);
setActiveId(w.id);
} else if (activeId === w.id) {
minimizeWin(w.id);
} else {
focus(w.id);
}
};
const wallpaper = `url('${WALLPAPER}')`;
return (
<div
className="win-desktop"
style={{ backgroundImage: wallpaper }}
onPointerDown={(e) => {
if (e.target === e.currentTarget) {
setStartOpen(false);
setSelectedIcon(null);
}
}}
>
<div className="win-wallpaper-tint" aria-hidden="true" />
{/* Desktop icons */}
<div className="win-icons" onPointerDown={(e) => e.stopPropagation()}>
{APPS.map((app) => (
<button
key={app.id}
type="button"
className={"win-icon" + (selectedIcon === app.id ? " selected" : "")}
onClick={() => setSelectedIcon(app.id)}
onDoubleClick={() => openApp(app.id)}
title={`Open ${app.title}`}
>
<span className="win-icon-glyph" style={{ background: app.color }}>
{app.icon}
</span>
<span className="win-icon-label">{app.title}</span>
</button>
))}
</div>
{/* Windows */}
<div className="win-layer">
{wins.map((w) => (
<WinWindow
key={w.id}
win={w}
app={APP_BY_ID[w.appId]}
active={activeId === w.id}
onFocus={() => focus(w.id)}
onClose={() => closeWin(w.id)}
onMinimize={() => minimizeWin(w.id)}
onToggleMaximize={() => toggleMax(w.id)}
onMove={(x, y) => moveWin(w.id, x, y)}
/>
))}
</div>
{/* Start menu */}
{startOpen && (
<div className="win-startmenu" onPointerDown={(e) => e.stopPropagation()}>
<div className="win-startmenu-user">
<span className="win-startmenu-avatar">🧑‍💻</span>
<span>Martin Dvořák</span>
</div>
<div className="win-startmenu-list">
{APPS.map((app) => (
<button
key={app.id}
type="button"
className="win-startmenu-item"
onClick={() => openApp(app.id)}
>
<span className="win-startmenu-icon" style={{ background: app.color }}>
{app.icon}
</span>
{app.title}
</button>
))}
</div>
<div className="win-startmenu-foot">
<button
type="button"
className="win-startmenu-power"
onClick={() => {
toggle();
setStartOpen(false);
}}
>
⏻ Switch to {theme === "skeuo" ? "Terminal" : "Aero"}
</button>
</div>
</div>
)}
{/* Taskbar */}
<div className="win-taskbar" onPointerDown={(e) => e.stopPropagation()}>
<button
type="button"
className={"win-start" + (startOpen ? " open" : "")}
onClick={() => setStartOpen((v) => !v)}
aria-label="Start"
>
<span className="win-start-orb">⊞</span>
</button>
<div className="win-tb-buttons">
{wins.map((w) => {
const app = APP_BY_ID[w.appId];
return (
<button
key={w.id}
type="button"
className={
"win-tb-btn" +
(activeId === w.id && !w.minimized ? " active" : "") +
(w.minimized ? " minimized" : "")
}
onClick={() => taskbarClick(w)}
title={app.title}
>
<span className="win-tb-btn-icon" style={{ background: app.color }}>
{app.icon}
</span>
<span className="win-tb-btn-label">{app.title}</span>
</button>
);
})}
</div>
<div className="win-tray">
<form
className="win-search"
action="https://search.dvorakmortonxd.dev/search"
method="GET"
target="_blank"
rel="noopener noreferrer"
>
<span className="win-search-glyph" aria-hidden="true">
⌕
</span>
<input
type="text"
name="q"
placeholder="Search the web..."
aria-label="Search the web"
/>
</form>
<button
type="button"
className="win-theme-toggle"
onClick={toggle}
title={`Switch to ${theme === "skeuo" ? "terminal" : "Aero"} theme`}
>
<span aria-hidden="true">{theme === "skeuo" ? "▚" : "❖"}</span>
<span>{theme === "skeuo" ? "Terminal" : "Aero"}</span>
</button>
<Clock className="win-clock" />
<button type="button" className="win-showdesktop" aria-label="Show desktop" onClick={() => setWins((prev) => prev.map((w) => ({ ...w, minimized: true })))} />
</div>
</div>
</div>
);
}

426
src/app/components/apps.tsx Normal file
View file

@ -0,0 +1,426 @@
"use client";
import Image from "next/image";
import type { ReactNode } from "react";
import {
socials,
codingProjects,
techProjects,
friends,
WALLPAPER_CREDIT,
} from "../data";
import ServiceButton from "./ServiceButton";
import ContributionHeatmap from "./ContributionHeatmap";
import JokerLogo from "./JokerLogo";
export interface AppDef {
id: string;
title: string;
icon: string;
color: string;
width: number;
height: number;
content: ReactNode;
}
function WelcomeApp() {
return (
<div className="app-pad app-welcome">
<div className="app-welcome-head">
<JokerLogo />
<div>
<h2>Welcome to dvorakmortonxd.dev</h2>
<p className="app-muted">A Windows 7 desktop edition of my personal website.</p>
</div>
</div>
<p>
Double-click an icon on the desktop (or open the
<strong> Start </strong> menu) to launch an application. Everything you used to
scroll through now lives inside its own little window.
</p>
<ul className="app-list">
<li>Drag windows around by their title bar.</li>
<li>Use the buttons at the top-right to minimize, maximize or close.</li>
<li>Search the web from the taskbar at the bottom.</li>
<li>Switch back to the terminal theme from the tray, bottom-right.</li>
</ul>
<p className="app-muted app-fineprint">
Wallpaper from{" "}
<a href={WALLPAPER_CREDIT} target="_blank" rel="noopener noreferrer">
wallpaperaccess.com/open-source
</a>
.
</p>
</div>
);
}
function AboutApp() {
return (
<div className="app-pad">
<p className="app-cmd">
<span className="app-user">martin@dvorakmortonxd</span>:<span className="app-path">~</span>$
cat about_me.md
</p>
<div className="app-text">
<p>
Hey, I&#39;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{" "}
<a href="https://sssvt.cz">sssvt.cz</a>. I&#39;m a sophomore and I have wanted to
go to this school since 7th grade (2022) so I&#39;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&#39;m a homelabber and developer heavily focused on
self-hosting <i>literally everything</i>, 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&#39;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.
</p>
</div>
</div>
);
}
function NowApp() {
return (
<div className="app-pad">
<p className="app-cmd">
<span className="app-user">martin@dvorakmortonxd</span>:<span className="app-path">~</span>$
cat now.txt
</p>
<div className="app-text">
<p>
I&#39;m currently hosting an SMP (dm me on discord). Also I&#39;m pretty much
cooking daily, because im bulking through out the non-summer seasons. And me and
my pal <a href="https://skopec.dev">Pepa</a> are working on an education app so we
can study more efficiently, It&#39;s called{" "}
<a href="https://git.dvorakmortonxd.dev/edu/maturita.c">maturita.c</a>!
</p>
</div>
</div>
);
}
function SocialsApp() {
return (
<div className="app-pad">
<div className="social-buttons">
{socials.map((s) => (
<a
key={s.name}
href={s.url}
target="_blank"
rel="noopener noreferrer"
className="social-button"
>
<Image src={s.icon} alt={s.name} width={16} height={16} className="social-icon" />
<span className="social-label">{s.name}</span>
</a>
))}
</div>
<div className="mail-contact">
mail if u ever wanna contact me:{" "}
<a href="mailto:dvorak@dvorakmortonxd.dev">dvorak@dvorakmortonxd.dev</a>
</div>
</div>
);
}
function ServicesApp() {
return (
<div className="app-pad">
<div className="services-buttons services-main">
<ServiceButton
url="https://proxmox.dvorakmortonxd.dev/"
className="proxmox"
icon="🖥️"
name="Proxmox"
desc="Server Management"
access="private"
disabled
/>
<ServiceButton
url="https://ai.dvorakmortonxd.dev/"
className="ai"
icon="🦾"
name="Llama.cpp"
desc="Local AI Server"
access="on_request"
/>
<ServiceButton
url="https://jelly.dvorakmortonxd.dev/"
className="jellyfin"
icon="🎬"
name="Jellyfin"
desc="Local Media Server"
access="on_request"
/>
<ServiceButton
url="https://discord.gg/vyvhk7d8bX"
checkUrl="minecraft://mc.dvorakmortonxd.dev:25565"
className="pterodactyl"
icon="🎮"
name="SMP"
desc="My Survival Minecraft Server"
access="on_request"
/>
<ServiceButton
url="https://navidrome.dvorakmortonxd.dev/"
className="funkwhale"
icon="🎸"
name="Navidrome"
desc="Music Streaming"
access="private"
/>
<ServiceButton
url="https://git.dvorakmortonxd.dev"
className="gitdxd"
icon="🐙"
name="GitDXD"
desc="Best Git Hosting OAT"
access="public"
/>
</div>
</div>
);
}
function ProjectsApp() {
return (
<div className="app-pad">
<div className="project-grid">
{codingProjects.map((project, idx) => (
<a
key={idx}
href={project.url}
target="_blank"
rel="noopener noreferrer"
className="project-card"
>
<div className="project-image" style={{ backgroundImage: `url(${project.img})` }} />
<h4 className="project-title">{project.title}</h4>
<p className="project-desc">{project.desc}</p>
</a>
))}
</div>
</div>
);
}
function HardwareApp() {
return (
<div className="app-pad">
<div className="project-grid">
{techProjects.map((project) => (
<div key={project.title} className="project-card">
<div
className="project-image"
style={{ backgroundImage: `url(${project.image})` }}
/>
<h4 className="project-title">{project.title}</h4>
<p className="project-desc">{project.desc}</p>
</div>
))}
</div>
</div>
);
}
function FriendsApp() {
return (
<div className="app-pad">
<div className="services-buttons" style={{ gap: "1rem" }}>
{friends.map((friend, idx) => (
<ServiceButton
key={idx}
url={friend.realUrl || friend.url}
className="friend"
icon={
<Image
src={friend.img}
alt={friend.name}
width={68}
height={68}
className="friend-icon"
style={{ borderRadius: "50%" }}
/>
}
name={friend.name}
desc={friend.desc}
/>
))}
</div>
<div className="friends-extra">
<h3>Quipee by Bukovina Filip &amp; Vlk Tomáš</h3>
<div className="app-shot">
<Image
src="/quipee.webp"
alt="Quipee Thumbnail"
width={1280}
height={720}
style={{ width: "100%", height: "auto", display: "block" }}
/>
</div>
<div className="app-linkrow">
<a href="https://quipee.me" target="_blank" rel="noopener noreferrer">
Web
</a>
<a
href="https://drive.google.com/drive/folders/1oi76JyWs6J84MUmCGV7GvtLUexXI8Jel?usp=share_link"
target="_blank"
rel="noopener noreferrer"
>
Android
</a>
<a
href="https://testflight.apple.com/join/hP2a1E5x"
target="_blank"
rel="noopener noreferrer"
>
IOS
</a>
</div>
</div>
</div>
);
}
function GitdxdApp() {
return (
<div className="app-pad app-center">
<h2 className="app-h1">GitDXD</h2>
<p className="app-h2">My Self-hosted Git Instance</p>
<div className="app-shot app-shot-glow">
<Image
src="/gitdxd.webp"
alt="GitDXD Preview"
width={1280}
height={720}
style={{ width: "100%", height: "auto", display: "block" }}
/>
</div>
<p className="app-text">
My very own forgejo git instance, originally started as a private git instance for
me and my friends to boycott GitHub but I&#39;ve made it public now so. Join at{" "}
<a href="https://git.dvorakmortonxd.dev">git.dvorakmortonxd.dev</a>.
</p>
</div>
);
}
function ContributionsApp() {
return (
<div className="app-pad">
<ContributionHeatmap />
</div>
);
}
function GymApp() {
return (
<div className="app-pad">
<div className="gym-grid">
<div className="gym-item">
<Image src="/squat.svg" alt="Squat" width={48} height={48} className="gym-icon" />
<div className="gym-label">SQUAT</div>
<div className="gym-weight">125kg</div>
<div className="gym-date">5.1.2025</div>
</div>
<div className="gym-item">
<Image src="/bench.svg" alt="Bench" width={48} height={48} className="gym-icon" />
<div className="gym-label">BENCH</div>
<div className="gym-weight">111kg</div>
<div className="gym-date">29.5.2026</div>
</div>
<div className="gym-item">
<Image
src="/deadlift.svg"
alt="Deadlift"
width={48}
height={48}
className="gym-icon"
/>
<div className="gym-label">DEADLIFT</div>
<div className="gym-weight">165kg</div>
<div className="gym-date">17.12.2024</div>
</div>
</div>
</div>
);
}
function RevivalApp() {
return (
<div className="app-pad app-center">
<h2 className="app-h1">Revival Inc.</h2>
<div className="app-shot app-shot-light">
<Image
src="/revivalinc.webp"
alt="revival inc thumbnail"
width={768}
height={576}
style={{ width: "100%", height: "auto", display: "block" }}
/>
</div>
<p className="app-h2">What is it?</p>
<p className="app-text">
It&#39;s a game I made for the game jam &#34;Mini Jame Gam #55&#34;, 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{" "}
<a href="https://martin-dv.itch.io/revival-inc">itch.io</a> or play{" "}
<a href="https://dvorakmortonxd.dev/revival-inc">here</a>.
</p>
</div>
);
}
function RustyWolfApp() {
return (
<div className="app-pad app-center">
<h2 className="app-h1">Introducing RustyWolf</h2>
<div className="app-shot app-shot-glow">
<Image
src="/RustyWolfPreview.webp"
alt="RustyWolf Browser Preview"
width={1280}
height={720}
style={{ width: "100%", height: "auto", display: "block" }}
/>
</div>
<p className="app-h2">What makes it so cool???</p>
<p className="app-text">
A lightweight browser made in Rust with WebKitGTK. It&#39;s the successor to my
previous project Venturesome, aiming to provide a fast and minimalist browsing
experience. It&#39;s a perfect browser for old machines that struggle with modern
browsers. Check it out at{" "}
<a href="https://git.dvorakmortonxd.dev/dvorakmortonxd/RustyWolf">GitDXD</a>.
</p>
</div>
);
}
export const APPS: AppDef[] = [
{ id: "welcome", title: "Welcome", icon: "👋", color: "#2f7fc4", width: 620, height: 430, content: <WelcomeApp /> },
{ id: "about", title: "About Me", icon: "🧑‍💻", color: "#0d5aa7", width: 720, height: 470, content: <AboutApp /> },
{ id: "now", title: "now.txt", icon: "📝", color: "#6a4fb3", width: 560, height: 320, content: <NowApp /> },
{ id: "socials", title: "Socials", icon: "🔗", color: "#0a7cc1", width: 700, height: 460, content: <SocialsApp /> },
{ id: "services", title: "Services", icon: "🖥️", color: "#157347", width: 760, height: 480, content: <ServicesApp /> },
{ id: "projects", title: "Projects", icon: "🎮", color: "#b3005f", width: 860, height: 560, content: <ProjectsApp /> },
{ id: "hardware", title: "Hardware", icon: "💻", color: "#1257a0", width: 860, height: 560, content: <HardwareApp /> },
{ id: "friends", title: "Friends", icon: "🫂", color: "#c0392b", width: 780, height: 560, content: <FriendsApp /> },
{ id: "gitdxd", title: "GitDXD", icon: "🐙", color: "#333333", width: 780, height: 560, content: <GitdxdApp /> },
{ id: "contributions", title: "Contributions", icon: "📊", color: "#157347", width: 900, height: 470, content: <ContributionsApp /> },
{ id: "gym", title: "Gym SBD", icon: "🏋️", color: "#c0392b", width: 620, height: 380, content: <GymApp /> },
{ id: "revival", title: "Revival Inc.", icon: "🧟", color: "#6a4fb3", width: 740, height: 560, content: <RevivalApp /> },
{ id: "rustywolf", title: "RustyWolf", icon: "🐺", color: "#0a7cc1", width: 780, height: 560, content: <RustyWolfApp /> },
];
export const APP_BY_ID: Record<string, AppDef> = Object.fromEntries(
APPS.map((a) => [a.id, a])
);

View file

@ -0,0 +1,45 @@
"use client";
import { useSyncExternalStore } from "react";
export type Theme = "skeuo" | "terminal";
const EVENT = "theme-change";
function subscribe(callback: () => void) {
window.addEventListener(EVENT, callback);
window.addEventListener("storage", callback);
return () => {
window.removeEventListener(EVENT, callback);
window.removeEventListener("storage", callback);
};
}
function getSnapshot(): Theme {
return document.documentElement.getAttribute("data-theme") === "terminal"
? "terminal"
: "skeuo";
}
function getServerSnapshot(): Theme {
return "skeuo";
}
export function setTheme(theme: Theme) {
if (typeof document === "undefined") return;
document.documentElement.setAttribute("data-theme", theme);
try {
localStorage.setItem("theme", theme);
} catch {
/* ignore */
}
window.dispatchEvent(new Event(EVENT));
}
export function useTheme() {
const theme = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
return {
theme,
toggle: () => setTheme(theme === "skeuo" ? "terminal" : "skeuo"),
};
}