first commit/push

This commit is contained in:
dvorakmortonxd 2026-05-13 13:20:51 +02:00
commit cc60991fc0
27 changed files with 11489 additions and 1 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

6
.gitignore vendored
View file

@ -1,3 +1,4 @@
<<<<<<< HEAD
# ---> Nextjs
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
@ -182,3 +183,8 @@ vite.config.js.timestamp-*
vite.config.ts.timestamp-*
.vite/
=======
node_modules/
.next/
.idea/
>>>>>>> 2b92431 (first commit/push)

View file

@ -1,3 +1,98 @@
# dvorakmortonxd.dev
The code for the website https://dvorakmortonxd.dev/
> My personal website — socials, projects, services, and hardware all in one place. Welcome to the goblin cave. >:)
![Next.js](https://img.shields.io/badge/Next.js-black?logo=next.js) ![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?logo=typescript&logoColor=white) ![Gentoo](https://img.shields.io/badge/Gentoo-54487A?logo=gentoo&logoColor=white)
---
## $ whoami
Hey, I'm **Martin Dvořák** — a 17-year-old IT student based in the Czech Republic, specializing in network management and administration at [sssvt.cz](https://sssvt.cz). I'm a passionate homelabber and developer heavily focused on self-hosting literally everything, writing code, and tinkering with Linux systems and retro hardware.
---
## 🌐 Live Site
**[dvorakmortonxd.dev](https://dvorakmortonxd.dev)**
---
## 🛠️ Tech Stack
- **Framework:** Next.js (TypeScript)
- **Styling:** PostCSS / Tailwind
- **Deployed at:** [dvorakmortonxd.dev](https://dvorakmortonxd.dev)
---
## 📁 Project Structure
```
dvorakmortonxd.dev/
├── public/ # Static assets (icons, SVGs)
├── src/ # App source code
├── next.config.ts
├── tsconfig.json
└── postcss.config.mjs
```
---
## 🚀 Running Locally
```bash
git clone https://git.dvorakmortonxd.dev/dvorakmortonxd/dvorakmortonxd.dev
cd dvorakmortonxd.dev
npm install
npm run dev
```
Open [http://localhost:3000](http://localhost:3000) in your browser.
---
## 🖥️ Self-Hosted Services
| Service | Description | Link |
|---|---|---|
| Proxmox | Server Management | [proxmox.dvorakmortonxd.dev](https://proxmox.dvorakmortonxd.dev) |
| Ollama | Local AI Server | [ai.dvorakmortonxd.dev](https://ai.dvorakmortonxd.dev) |
| Jellyfin | Local Media Server | [jelly.dvorakmortonxd.dev](https://jelly.dvorakmortonxd.dev) |
| MCSManager | Game Server Panel | [fake-pterodactyl.dvorakmortonxd.dev](https://fake-pterodactyl.dvorakmortonxd.dev) |
| Navidrome | Music Streaming | [navidrome.dvorakmortonxd.dev](https://navidrome.dvorakmortonxd.dev) |
| Mailcow | Email Server | [mailcow.dvorakmortonxd.dev](https://mailcow.dvorakmortonxd.dev) |
| SearxNG | Private Search | [search.dvorakmortonxd.dev](https://search.dvorakmortonxd.dev) |
| ConvertX | File Converter | [convert.dvorakmortonxd.dev](https://convert.dvorakmortonxd.dev) |
| GitDXD | Self-hosted Git | [git.dvorakmortonxd.dev](https://git.dvorakmortonxd.dev) |
---
## 🧪 Projects
- **[Hospital Execution](https://martin-dv.itch.io/hospital-execution-demo)** — First real game, made in Pygame
- **[Underbaranoia](https://martin-dv.itch.io/underbaranoia)** — Undertale-like infinite fight simulator in Pygame
- **[RustyWolf](https://git.dvorakmortonxd.dev/dvorakmortonxd/RustyWolf)** — Lightweight browser written in Rust with WebKitGTK
- **[dxdwm](https://git.dvorakmortonxd.dev/dvorakmortonxd/dxdwm)** — Small X11 window manager written in Rust
- **[Venturesome](https://git.dvorakmortonxd.dev/dvorakmortonxd/Venturesome)** — Browser written in Python with PyQtWebEngine
- **[Mandelbrot Set in Pygame](https://git.dvorakmortonxd.dev/dvorakmortonxd/mandelbrot)** — Pygame Mandelbrot set renderer
- **[Underpygametale](https://git.dvorakmortonxd.dev/dvorakmortonxd/underpygametale)** — Undertale remake in Pygame
- **[Batch MSDOS Simulator](https://git.dvorakmortonxd.dev/dvorakmortonxd/DvorakMSDOS)** — Batch OS simulator with games, browser, and surprises
- **[Offline Memer](https://martin-dv.itch.io/offline-memer)** — Dank Memer parody made in Batch
- **[Softcraft](https://dvorakmortonxd.dev/softcraft)** — My Minecraft server with a [live BlueMap](https://bluemap.dvorakmortonxd.dev)
---
## 📬 Contact
- **Email:** [martin@dvorakmortonxd.dev](mailto:martin@dvorakmortonxd.dev)
- **Instagram:** [@dvorakmortonxd](https://www.instagram.com/dvorakmortonxd/)
- **LinkedIn:** [Martin Dvořák](https://www.linkedin.com/in/martin-dvorak-32012a394/)
- **Discord:** [dvorakmortonxd](https://discord.com/users/555745543763001364)
- **Itch.io:** [martin-dv.itch.io](https://martin-dv.itch.io)
---
## 📄 License
© 2025–2026 Martin Dvořák. All rights reserved (not really, I don't care).

25
eslint.config.mjs Normal file
View file

@ -0,0 +1,25 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
{
ignores: [
"node_modules/**",
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
],
},
];
export default eslintConfig;

6
next-env.d.ts vendored Normal file
View file

@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

14
next.config.ts Normal file
View file

@ -0,0 +1,14 @@
import { NextConfig } from "next";
const nextConfig: NextConfig = {
webpack(config) {
config.module.rules.push({
test: /\.svg$/i,
issuer: /\.[jt]sx?$/,
use: ["@svgr/webpack"],
});
return config;
},
};
export default nextConfig;

8336
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

31
package.json Normal file
View file

@ -0,0 +1,31 @@
{
"name": "dvorakmortonxd",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build --turbopack",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"next": "^15.5.18",
"nextjs": "^0.0.3",
"react": "19.1.0",
"react-dom": "19.1.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@svgr/webpack": "^8.1.0",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"autoprefixer": "^10.4.21",
"eslint": "^9",
"eslint-config-next": "15.5.4",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.14",
"typescript": "^5"
}
}

5
postcss.config.mjs Normal file
View file

@ -0,0 +1,5 @@
const config = {
plugins: ["@tailwindcss/postcss"],
};
export default config;

BIN
src/.DS_Store vendored Normal file

Binary file not shown.

BIN
src/app/.DS_Store vendored Normal file

Binary file not shown.

35
src/app/api/ping/route.ts Normal file
View file

@ -0,0 +1,35 @@
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const url = searchParams.get('url');
if (!url) return NextResponse.json({ status: 'down' });
if (url.startsWith('minecraft://')) {
const address = url.replace('minecraft://', '');
try {
const res = await fetch(`https://api.mcsrvstat.us/3/${address}`);
const data = await res.json();
return NextResponse.json({ status: data.online ? 'up' : 'down' });
} catch {
return NextResponse.json({ status: 'down' });
}
}
try {
const httpRes = await fetch(url, {
method: 'GET',
signal: AbortSignal.timeout(5000)
});
// Accept all non-5xx responses as "up". 5xx usually means origin or gateway errors (like Cloudflare 521, 522, etc.)
if (httpRes && httpRes.status < 500) {
return NextResponse.json({ status: 'up' });
}
} catch (e) {
console.error(`Failed to ping ${url}`, e);
}
return NextResponse.json({ status: 'down' });
}

BIN
src/app/components/.DS_Store vendored Normal file

Binary file not shown.

View file

@ -0,0 +1,36 @@
"use client";
import { useState } from "react";
import Image from "next/image";
export default function JokerLogo() {
const [jokerActive, setJokerActive] = useState(false);
const handleClick = () => {
if (jokerActive) return;
setJokerActive(true);
setTimeout(() => {
setJokerActive(false);
}, 1200);
};
return (
<>
<Image
src="/icon.png"
alt="Logo"
className="logo"
width={100}
height={100}
onClick={handleClick}
style={{ cursor: "pointer" }}
/>
{jokerActive && (
<div className="joker-flash-overlay">
<Image src="/joker.png" alt="Joker" className="joker-img" width={500} height={500} unoptimized />
</div>
)}
</>
);
}

View file

@ -0,0 +1,42 @@
"use client";
import React, { useEffect, useState, ReactNode } from 'react';
export default function ServiceButton({ url, checkUrl, className, icon, name, desc }: { url: string, checkUrl?: string, className: string, icon: ReactNode, name: string, desc: string }) {
const [status, setStatus] = useState<'pending' | 'up' | 'down'>('pending');
useEffect(() => {
const checkStatus = async () => {
try {
const targetUrl = checkUrl || url;
const res = await fetch(`/api/ping?url=${encodeURIComponent(targetUrl)}`);
const data = await res.json();
setStatus(data.status);
} catch {
setStatus('down');
}
};
checkStatus();
}, [url, checkUrl]);
return (
<a href={url} target="_blank" rel="noopener noreferrer" className={`service-button ${className}`} style={{ position: 'relative' }}>
<div style={{
position: 'absolute',
top: '8px',
right: '8px',
width: '10px',
height: '10px',
borderRadius: '50%',
backgroundColor: status === 'up' ? '#4ade80' : status === 'down' ? '#ef4444' : '#9ca3af',
boxShadow: status === 'up' ? '0 0 5px #4ade80' : status === 'down' ? '0 0 5px #ef4444' : 'none',
transition: 'all 0.3s ease'
}} title={status === 'up' ? 'Active' : status === 'down' ? 'Dead' : 'Checking...'} />
<span className="service-icon">{icon}</span>
<span className="service-text">
<span className="service-name">{name}</span>
<span className="service-desc">{desc}</span>
</span>
</a>
);
}

View file

@ -0,0 +1,105 @@
"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_|
\\_/ \\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 > 30) {
setPos((prev) => {
if (prev <= -150) return 150;
return prev - 1.5;
});
lastTime = timestamp;
}
animationFrameId = requestAnimationFrame(animate);
};
animationFrameId = requestAnimationFrame(animate);
return () => cancelAnimationFrame(animationFrameId);
}, []);
return (
<>
<style>
{`
@keyframes rainbowShift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
`}
</style>
<div
style={{
position: "fixed",
top: 0,
left: 0,
width: "100%",
height: "100%",
pointerEvents: "none",
zIndex: 0,
overflow: "hidden",
display: "flex",
alignItems: "flex-end",
paddingBottom: "5vh",
justifyContent: "center",
userSelect: "none",
backgroundImage: "linear-gradient(rgba(29, 32, 33, 0.94), rgba(29, 32, 33, 0.94)), url('/gruvbox.png')",
backgroundSize: "cover",
backgroundPosition: "center",
}}
>
<pre
style={{
margin: 0,
whiteSpace: "pre",
fontFamily: "var(--font-custom-mono), monospace",
fontSize: "1rem",
transform: "translateX(" + pos + "vw)",
willChange: "transform",
background: "linear-gradient(90deg, var(--red), var(--accent2), var(--accent), var(--green), var(--aqua), var(--blue), var(--purple))",
backgroundSize: "200% auto",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
animation: "rainbowShift 3s linear infinite",
opacity: 0.35,
filter: "drop-shadow(0 0 12px rgba(250, 189, 47, 0.5))",
}}
>
{TRAIN_ASCII}
</pre>
</div>
</>
);
}

710
src/app/darecek/darecek.css Normal file
View file

@ -0,0 +1,710 @@
/* Valentine's Page for Kiki */
.kiki-container {
min-height: 100vh;
width: 100%;
background: linear-gradient(
135deg,
#1a1a2e 0%,
#16213e 25%,
#1a1a2e 50%,
#2d132c 75%,
#1a1a2e 100%
);
background-size: 400% 400%;
animation: gradientBG 15s ease infinite;
position: relative;
overflow: hidden;
font-family: "Inter", "Segoe UI", system-ui, sans-serif;
}
@keyframes gradientBG {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
/* Floating Hearts */
.hearts-container {
position: fixed;
inset: 0;
pointer-events: none;
overflow: hidden;
z-index: 1;
}
.floating-heart {
position: absolute;
bottom: -50px;
animation: floatUp linear infinite;
opacity: 0.7;
filter: drop-shadow(0 0 10px rgba(255, 105, 180, 0.5));
}
@keyframes floatUp {
0% {
transform: translateY(0) rotate(0deg) scale(1);
opacity: 0;
}
10% {
opacity: 0.7;
}
90% {
opacity: 0.7;
}
100% {
transform: translateY(-110vh) rotate(720deg) scale(0.5);
opacity: 0;
}
}
/* Sparkles */
.sparkles {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 2;
}
.sparkle {
position: absolute;
width: 6px;
height: 6px;
background: white;
border-radius: 50%;
animation: sparkle 3s ease-in-out infinite;
}
@keyframes sparkle {
0%, 100% {
opacity: 0;
transform: scale(0);
}
50% {
opacity: 1;
transform: scale(1);
box-shadow:
0 0 10px #fff,
0 0 20px #ff69b4,
0 0 30px #ff69b4;
}
}
/* Rose Petals */
.petals-container {
position: fixed;
inset: 0;
pointer-events: none;
overflow: hidden;
z-index: 3;
}
.petal {
position: absolute;
top: -50px;
font-size: 24px;
animation: fallPetal linear infinite;
filter: drop-shadow(0 0 5px rgba(255, 182, 193, 0.5));
}
@keyframes fallPetal {
0% {
transform: translateY(0) rotate(0deg) translateX(0);
opacity: 0;
}
10% {
opacity: 0.8;
}
90% {
opacity: 0.8;
}
100% {
transform: translateY(110vh) rotate(360deg) translateX(100px);
opacity: 0;
}
}
/* Main Content */
.kiki-main {
position: relative;
z-index: 10;
display: flex;
flex-direction: column;
align-items: center;
padding: 60px 20px;
min-height: 100vh;
}
/* Title */
.kiki-title {
font-size: clamp(2.5rem, 8vw, 5rem);
font-weight: 900;
text-align: center;
background: linear-gradient(
135deg,
#ff6b9d 0%,
#ff8fab 20%,
#ffc3d8 40%,
#ff8fab 60%,
#ff6b9d 80%,
#e84a8a 100%
);
background-size: 300% 300%;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
animation: titleGradient 5s ease-in-out infinite;
margin-bottom: 0.5rem;
opacity: 0;
transform: translateY(30px);
transition: all 1s cubic-bezier(0.34, 1.56, 0.64, 1);
filter: drop-shadow(0 0 30px rgba(255, 107, 157, 0.5));
}
.kiki-title.show {
opacity: 1;
transform: translateY(0);
}
@keyframes titleGradient {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
.kiki-subtitle {
font-size: clamp(2rem, 6vw, 4rem);
font-weight: 800;
color: #ffc3d8;
text-align: center;
margin-bottom: 3rem;
opacity: 0;
transform: translateY(30px);
transition: all 1s cubic-bezier(0.34, 1.56, 0.64, 1) 0.2s;
text-shadow:
0 0 20px rgba(255, 195, 216, 0.6),
0 0 40px rgba(255, 107, 157, 0.4),
0 0 60px rgba(255, 107, 157, 0.2);
animation: subtitlePulse 2s ease-in-out infinite;
}
.kiki-subtitle.show {
opacity: 1;
transform: translateY(0);
}
@keyframes subtitlePulse {
0%, 100% {
text-shadow:
0 0 20px rgba(255, 195, 216, 0.6),
0 0 40px rgba(255, 107, 157, 0.4),
0 0 60px rgba(255, 107, 157, 0.2);
}
50% {
text-shadow:
0 0 30px rgba(255, 195, 216, 0.8),
0 0 60px rgba(255, 107, 157, 0.6),
0 0 90px rgba(255, 107, 157, 0.4);
}
}
/* Photo Frame */
.photo-frame {
width: 100%;
max-width: 400px;
margin: 0 auto 3rem;
opacity: 0;
transform: scale(0.8) rotateY(-10deg);
transition: all 1.2s cubic-bezier(0.34, 1.56, 0.64, 1) 0.4s;
perspective: 1000px;
}
.photo-frame.show {
opacity: 1;
transform: scale(1) rotateY(0deg);
}
.photo-inner {
background: linear-gradient(145deg, #ff6b9d, #ff8fab, #ffc3d8);
padding: 12px;
border-radius: 24px;
box-shadow:
0 25px 80px rgba(255, 107, 157, 0.4),
0 10px 30px rgba(0, 0, 0, 0.3),
inset 0 2px 0 rgba(255, 255, 255, 0.3);
overflow: hidden;
position: relative;
}
.photo-inner::before {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.2) 0%,
transparent 50%
);
pointer-events: none;
border-radius: 24px;
}
.photo-slider {
display: flex;
transition: transform 0.8s cubic-bezier(0.4, 0, 0.2, 1);
}
.photo-slide {
min-width: 100%;
}
.photo {
width: 100%;
height: 350px;
object-fit: cover;
border-radius: 16px;
display: block;
}
.photo-dots {
display: flex;
justify-content: center;
gap: 10px;
margin-top: 16px;
}
.photo-dot {
width: 12px;
height: 12px;
border-radius: 50%;
border: none;
background: rgba(255, 107, 157, 0.3);
cursor: pointer;
transition: all 0.3s ease;
}
.photo-dot:hover {
background: rgba(255, 107, 157, 0.6);
transform: scale(1.2);
}
.photo-dot.active {
background: #ff6b9d;
box-shadow: 0 0 15px rgba(255, 107, 157, 0.8);
transform: scale(1.3);
}
/* Love Letter */
.love-letter {
width: 100%;
max-width: 600px;
margin: 0 auto 3rem;
opacity: 0;
transform: translateY(50px) rotateX(-10deg);
transition: all 1.2s cubic-bezier(0.34, 1.56, 0.64, 1) 0.6s;
}
.love-letter.show {
opacity: 1;
transform: translateY(0) rotateX(0deg);
}
.letter-content {
background: linear-gradient(
145deg,
rgba(255, 255, 255, 0.08),
rgba(255, 255, 255, 0.03)
);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 107, 157, 0.3);
border-radius: 24px;
padding: 2.5rem;
box-shadow:
0 20px 60px rgba(0, 0, 0, 0.3),
0 0 40px rgba(255, 107, 157, 0.15),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
position: relative;
overflow: hidden;
}
.letter-content::before {
content: "💌";
position: absolute;
top: -10px;
right: -10px;
font-size: 80px;
opacity: 0.1;
transform: rotate(15deg);
}
.letter-greeting {
font-size: 1.5rem;
font-weight: 700;
color: #ff8fab;
margin-bottom: 1.5rem;
font-style: italic;
}
.letter-text {
color: #fff;
font-size: 1.1rem;
line-height: 1.8;
margin-bottom: 1.2rem;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
}
.letter-signature {
color: #ffc3d8;
font-size: 1.2rem;
text-align: right;
margin-top: 2rem;
font-style: italic;
line-height: 1.6;
}
.signature-name {
font-weight: 700;
font-size: 1.4rem;
background: linear-gradient(135deg, #ff6b9d, #ffc3d8);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Big Heart */
.big-heart {
margin: 2rem 0;
opacity: 0;
transform: scale(0);
transition: all 1s cubic-bezier(0.34, 1.56, 0.64, 1) 0.8s;
}
.big-heart.show {
opacity: 1;
transform: scale(1);
}
.heart-pulse {
font-size: 100px;
animation: heartBeat 1.5s ease-in-out infinite;
filter: drop-shadow(0 0 30px rgba(255, 107, 157, 0.6));
}
@keyframes heartBeat {
0%, 100% {
transform: scale(1);
}
10% {
transform: scale(1.15);
}
20% {
transform: scale(1);
}
30% {
transform: scale(1.15);
}
40% {
transform: scale(1);
}
}
/* Cute Messages */
.cute-messages {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 1rem;
margin-top: 2rem;
opacity: 0;
transform: translateY(30px);
transition: all 1s ease 1s;
}
.cute-messages.show {
opacity: 1;
transform: translateY(0);
}
.cute-msg {
background: linear-gradient(135deg, rgba(255, 107, 157, 0.3), rgba(255, 143, 171, 0.2));
backdrop-filter: blur(10px);
padding: 12px 24px;
border-radius: 999px;
color: #ffc3d8;
font-weight: 600;
font-size: 0.95rem;
border: 1px solid rgba(255, 107, 157, 0.3);
animation: floatMsg 3s ease-in-out infinite;
box-shadow: 0 5px 20px rgba(255, 107, 157, 0.2);
}
.cute-msg:nth-child(1) { animation-delay: 0s; }
.cute-msg:nth-child(2) { animation-delay: 0.5s; }
.cute-msg:nth-child(3) { animation-delay: 1s; }
@keyframes floatMsg {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
/* Responsive */
@media (max-width: 600px) {
.kiki-main {
padding: 40px 16px;
}
.photo {
height: 280px;
}
.letter-content {
padding: 1.5rem;
}
.letter-text {
font-size: 1rem;
}
.heart-pulse {
font-size: 70px;
}
.cute-messages {
flex-direction: column;
align-items: center;
}
}
/* Extra glow effect overlay */
.kiki-container::before {
content: "";
position: fixed;
top: 30%;
left: 50%;
transform: translateX(-50%);
width: 600px;
height: 600px;
background: radial-gradient(circle, rgba(255, 107, 157, 0.15) 0%, transparent 70%);
pointer-events: none;
z-index: 0;
animation: glowPulse 4s ease-in-out infinite;
}
@keyframes glowPulse {
0%, 100% { opacity: 0.5; transform: translateX(-50%) scale(1); }
50% { opacity: 1; transform: translateX(-50%) scale(1.2); }
}
/* Question Screen */
.question-screen {
position: relative;
z-index: 10;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 40px 20px;
text-align: center;
}
.question-heart {
font-size: 75px;
animation: heartBounce 2s ease infinite;
filter: drop-shadow(0 0 40px rgba(255, 107, 157, 0.6));
margin-bottom: 1rem;
}
@keyframes heartBounce {
0%, 100% { transform: scale(1) rotate(-5deg); }
25% { transform: scale(1.1) rotate(5deg); }
50% { transform: scale(1) rotate(-5deg); }
75% { transform: scale(1.15) rotate(5deg); }
}
.question-title {
font-size: clamp(2rem, 6vw, 3.5rem);
font-weight: 800;
color: #ffc3d8;
margin-bottom: 1rem;
text-shadow:
0 0 20px rgba(255, 195, 216, 0.6),
0 0 40px rgba(255, 107, 157, 0.4);
animation: fadeInDown 1s ease;
}
@keyframes fadeInDown {
from {
opacity: 0;
transform: translateY(-30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.question-text {
font-size: clamp(1.8rem, 5vw, 3rem);
font-weight: 900;
background: linear-gradient(
135deg,
#ff6b9d 0%,
#ff8fab 25%,
#ffc3d8 50%,
#ff8fab 75%,
#ff6b9d 100%
);
background-size: 300% 300%;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
animation: titleGradient 4s ease-in-out infinite, fadeInUp 1s ease 0.3s both;
margin-bottom: 3rem;
filter: drop-shadow(0 0 30px rgba(255, 107, 157, 0.5));
}
.question-buttons {
display: flex;
gap: 2rem;
align-items: center;
justify-content: center;
flex-wrap: wrap;
position: relative;
min-height: 80px;
width: 100%;
max-width: 500px;
}
.yes-button {
padding: 20px 50px;
font-size: 1.5rem;
font-weight: 700;
color: white;
background: linear-gradient(135deg, #ff6b9d, #ff8fab, #ff6b9d);
background-size: 200% 200%;
border: none;
border-radius: 999px;
cursor: pointer;
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
box-shadow:
0 10px 40px rgba(255, 107, 157, 0.5),
0 5px 15px rgba(0, 0, 0, 0.2),
inset 0 2px 0 rgba(255, 255, 255, 0.3);
animation: yesButtonPulse 2s ease-in-out infinite, fadeInUp 1s ease 0.5s both;
z-index: 10;
}
@keyframes yesButtonPulse {
0%, 100% {
transform: scale(1);
box-shadow:
0 10px 40px rgba(255, 107, 157, 0.5),
0 5px 15px rgba(0, 0, 0, 0.2),
inset 0 2px 0 rgba(255, 255, 255, 0.3);
}
50% {
transform: scale(1.05);
box-shadow:
0 15px 50px rgba(255, 107, 157, 0.7),
0 8px 25px rgba(0, 0, 0, 0.3),
inset 0 2px 0 rgba(255, 255, 255, 0.3);
}
}
.yes-button:hover {
transform: scale(1.15) !important;
background-position: 100% 0;
box-shadow:
0 20px 60px rgba(255, 107, 157, 0.7),
0 10px 30px rgba(0, 0, 0, 0.3),
inset 0 2px 0 rgba(255, 255, 255, 0.4);
}
.no-button {
padding: 15px 35px;
font-size: 1.1rem;
font-weight: 600;
color: #ff8fab;
background: rgba(30, 20, 40, 0.95);
border: 2px solid rgba(255, 107, 157, 0.5);
border-radius: 999px;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
backdrop-filter: blur(10px);
animation: fadeInUp 1s ease 0.6s both;
z-index: 5;
white-space: nowrap;
}
.no-button:hover {
background: rgba(50, 30, 60, 0.95);
border-color: rgba(255, 107, 157, 0.7);
}
.no-button.runaway {
z-index: 99999 !important;
animation: popIn 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275);
box-shadow:
0 10px 40px rgba(255, 107, 157, 0.5),
0 0 20px rgba(255, 107, 157, 0.3),
0 0 60px rgba(255, 107, 157, 0.2);
background: rgba(30, 20, 40, 0.98);
border: 2px solid rgba(255, 107, 157, 0.8);
pointer-events: auto;
}
@keyframes popIn {
0% {
transform: scale(0.3) rotate(-10deg);
opacity: 0;
}
60% {
transform: scale(1.15) rotate(3deg);
}
100% {
transform: scale(1) rotate(0deg);
opacity: 1;
}
}
.hint-text {
margin-top: 2rem;
color: #ffc3d8;
font-size: 1.1rem;
font-style: italic;
animation: fadeIn 0.5s ease;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* Make the Yes button grow bigger over time as No keeps running away */
.yes-button:active {
transform: scale(0.95) !important;
}
/* Responsive adjustments for question screen */
@media (max-width: 600px) {
.question-heart {
font-size: 70px;
}
.question-buttons {
flex-direction: column;
gap: 1.5rem;
}
.yes-button {
padding: 18px 45px;
font-size: 1.3rem;
}
.no-button {
padding: 12px 30px;
font-size: 1rem;
}
}

370
src/app/darecek/page.tsx Normal file
View file

@ -0,0 +1,370 @@
"use client";
import { useEffect, useState } from "react";
import Image from "next/image";
import "./darecek.css";
export default function KikiPage() {
const [hasAnswered, setHasAnswered] = useState(false);
const [showMessage, setShowMessage] = useState(false);
const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0);
const [hearts, setHearts] = useState<{ id: number; left: number; delay: number; duration: number; size: number }[]>([]);
const [noButtonStyle, setNoButtonStyle] = useState<React.CSSProperties>({});
const [noButtonText, setNoButtonText] = useState("ne 😢");
const [noHoverCount, setNoHoverCount] = useState(0);
const [isRunning, setIsRunning] = useState(false);
const noButtonTexts = [
"ne 😢",
"fakt? 🥺",
"pjosim? 🥺",
"hej 😡",
"stop 👿",
"nech toooo 😭",
"😭😭😭",
"budu urazeny 🥺",
"grrrrr 👿👿👿",
"stejnak nemas na vyber 😝😝😝",
];
// Add your photos here - just put images in /public/darecek/ folder
// named photo1.jpg, photo2.jpg, etc.
const photos = [
"/darecek/photo1.jpg",
"/darecek/photo2.jpg",
"/darecek/photo3.jpg",
"/darecek/photo4.jpg",
"/darecek/photo5.jpg",
];
const moveNoButton = (e: React.MouseEvent | React.TouchEvent) => {
// Get current mouse/touch position
let currentX = 0;
let currentY = 0;
if ('touches' in e) {
currentX = e.touches[0].clientX;
currentY = e.touches[0].clientY;
} else {
currentX = e.clientX;
currentY = e.clientY;
}
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const buttonWidth = 200;
const buttonHeight = 60;
const padding = 20;
const minDistanceFromMouse = 200; // Minimum distance from mouse
// Calculate safe bounds
const minX = padding;
const maxX = viewportWidth - buttonWidth - padding;
const minY = padding;
const maxY = viewportHeight - buttonHeight - padding;
let newX: number;
let newY: number;
let attempts = 0;
const maxAttempts = 50;
// Keep trying until we find a position far enough from mouse
do {
newX = minX + Math.random() * (maxX - minX);
newY = minY + Math.random() * (maxY - minY);
attempts++;
// Calculate distance from mouse
const distance = Math.sqrt(
Math.pow(newX + buttonWidth / 2 - currentX, 2) +
Math.pow(newY + buttonHeight / 2 - currentY, 2)
);
// If far enough from mouse, use this position
if (distance > minDistanceFromMouse) break;
} while (attempts < maxAttempts);
// If still too close after max attempts, move to opposite side of screen
const buttonCenterX = newX + buttonWidth / 2;
const buttonCenterY = newY + buttonHeight / 2;
const distanceFromMouse = Math.sqrt(
Math.pow(buttonCenterX - currentX, 2) +
Math.pow(buttonCenterY - currentY, 2)
);
if (distanceFromMouse < minDistanceFromMouse) {
// Move to opposite quadrant of the screen
if (currentX < viewportWidth / 2) {
newX = maxX - Math.random() * (maxX / 3);
} else {
newX = minX + Math.random() * (maxX / 3);
}
if (currentY < viewportHeight / 2) {
newY = maxY - Math.random() * (maxY / 3);
} else {
newY = minY + Math.random() * (maxY / 3);
}
}
// Ensure bounds
newX = Math.max(minX, Math.min(maxX, newX));
newY = Math.max(minY, Math.min(maxY, newY));
setNoButtonStyle({
position: 'fixed',
left: `${newX}px`,
top: `${newY}px`,
});
setIsRunning(true);
setNoHoverCount(prev => prev + 1);
setNoButtonText(noButtonTexts[Math.min(noHoverCount + 1, noButtonTexts.length - 1)]);
};
const handleYesClick = () => {
setHasAnswered(true);
// Show message after transition
setTimeout(() => setShowMessage(true), 500);
};
useEffect(() => {
// Generate floating hearts
const newHearts = Array.from({ length: 30 }, (_, i) => ({
id: i,
left: Math.random() * 100,
delay: Math.random() * 5,
duration: 5 + Math.random() * 10,
size: 15 + Math.random() * 25,
}));
setHearts(newHearts);
}, []);
useEffect(() => {
if (!hasAnswered) return;
// Auto-rotate photos
const interval = setInterval(() => {
setCurrentPhotoIndex((prev) => (prev + 1) % photos.length);
}, 4000);
return () => clearInterval(interval);
}, [hasAnswered, photos.length]);
// Question screen
if (!hasAnswered) {
return (
<div className="kiki-container">
{/* Floating Hearts Background */}
<div className="hearts-container">
{hearts.map((heart) => (
<div
key={heart.id}
className="floating-heart"
style={{
left: `${heart.left}%`,
animationDelay: `${heart.delay}s`,
animationDuration: `${heart.duration}s`,
fontSize: `${heart.size}px`,
}}
>
💖
</div>
))}
</div>
{/* Sparkles */}
<div className="sparkles">
{Array.from({ length: 50 }).map((_, i) => (
<div
key={i}
className="sparkle"
style={{
left: `${Math.random() * 100}%`,
top: `${Math.random() * 100}%`,
animationDelay: `${Math.random() * 3}s`,
}}
/>
))}
</div>
{/* Question Content */}
<div className="question-screen">
<div className="question-heart">😝</div>
<h1 className="question-title">kikiiiii</h1>
<h2 className="question-text">budes muj valentyn? ❤️</h2>
<div className="question-buttons">
<button className="yes-button" onClick={handleYesClick}>
jo 😘
</button>
{!isRunning && (
<button
className="no-button"
onMouseEnter={moveNoButton}
onTouchStart={moveNoButton}
onClick={moveNoButton}
>
{noButtonText}
</button>
)}
</div>
{noHoverCount > 3 && (
<p className="hint-text">
{noHoverCount > 6 ? "dej ano tele 😛" : "to tlacitko ti zdrha 😝😝😝"}
</p>
)}
</div>
{/* Runaway No Button - rendered outside the question-buttons container */}
{isRunning && (
<button
className="no-button runaway"
style={noButtonStyle}
onMouseEnter={moveNoButton}
onTouchStart={moveNoButton}
onClick={moveNoButton}
>
{noButtonText}
</button>
)}
</div>
);
}
return (
<div className="kiki-container">
{/* Floating Hearts Background */}
<div className="hearts-container">
{hearts.map((heart) => (
<div
key={heart.id}
className="floating-heart"
style={{
left: `${heart.left}%`,
animationDelay: `${heart.delay}s`,
animationDuration: `${heart.duration}s`,
fontSize: `${heart.size}px`,
}}
>
💘
</div>
))}
</div>
{/* Sparkles */}
<div className="sparkles">
{Array.from({ length: 50 }).map((_, i) => (
<div
key={i}
className="sparkle"
style={{
left: `${Math.random() * 100}%`,
top: `${Math.random() * 100}%`,
animationDelay: `${Math.random() * 3}s`,
}}
/>
))}
</div>
{/* Main Content */}
<main className="kiki-main">
{/* Title */}
<h1 className={`kiki-title ${showMessage ? "show" : ""}`}>
stastnej valentynnnn
</h1>
<h2 className={`kiki-subtitle ${showMessage ? "show" : ""}`}>
laskoooo 😘😘😝
</h2>
{/* Photo Gallery */}
<div className={`photo-frame ${showMessage ? "show" : ""}`}>
<div className="photo-inner">
<div
className="photo-slider"
style={{ transform: `translateX(-${currentPhotoIndex * 100}%)` }}
>
{photos.map((photo, index) => (
<div key={index} className="photo-slide">
<Image
src={photo}
alt={`Memory ${index + 1}`}
className="photo"
width={500}
height={500}
onError={(e) => {
// Fallback if image doesn't exist
(e.target as HTMLImageElement).src = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300' viewBox='0 0 300 300'%3E%3Crect fill='%23ff6b9d' width='300' height='300'/%3E%3Ctext x='150' y='150' text-anchor='middle' dy='.3em' fill='white' font-size='60'%3E💕%3C/text%3E%3C/svg%3E";
}}
/>
</div>
))}
</div>
</div>
<div className="photo-dots">
{photos.map((_, index) => (
<button
key={index}
className={`photo-dot ${index === currentPhotoIndex ? "active" : ""}`}
onClick={() => setCurrentPhotoIndex(index)}
/>
))}
</div>
</div>
{/* Love Letter */}
<div className={`love-letter ${showMessage ? "show" : ""}`}>
<div className="letter-content">
<p className="letter-greeting">pro moji princeznu,</p>
<p className="letter-text">
tohle je nas druhej valentyn a jsem moc moc stastnej, ze ho muzu stravit s tebou 😘
</p>
<p className="letter-text">
si ta nejlepsi, nejkrasnejsi a nejroztomilejsi holka na svete, kterou bych si kdy mohl prat, a jsem vdecnej, ze si to semnou vydrzela, i pres ty moje nalady 🥺😝❤️
</p>
<p className="letter-text">
tak strasne moc te milujuuuu a chci byt s tebou pro zbytek svyho zivota, lasko moje ❤️🥹
</p>
<p className="letter-signature">
tvoje laska,<br />
<span className="signature-name">Martin ❤️</span>
</p>
</div>
</div>
{/* Animated Heart */}
<div className={`big-heart ${showMessage ? "show" : ""}`}>
<div className="heart-pulse">❤️</div>
</div>
{/* Cute Messages */}
<div className={`cute-messages ${showMessage ? "show" : ""}`}>
<span className="cute-msg">jsi moje 👸🏽</span>
<span className="cute-msg">miluju te nejvic ve vesmiru 😝</span>
<span className="cute-msg">chci jenom tebee 😍</span>
</div>
</main>
{/* Rose petals falling */}
<div className="petals-container">
{Array.from({ length: 20 }).map((_, i) => (
<div
key={i}
className="petal"
style={{
left: `${Math.random() * 100}%`,
animationDelay: `${Math.random() * 8}s`,
animationDuration: `${8 + Math.random() * 7}s`,
}}
>
💝
</div>
))}
</div>
</div>
);
}

BIN
src/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

26
src/app/globals.css Normal file
View file

@ -0,0 +1,26 @@
@import "tailwindcss";
:root {
--background: #1d2021;
--foreground: #ebdbb2;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #1d2021;
--foreground: #ebdbb2;
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}

BIN
src/app/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 857 KiB

33
src/app/layout.tsx Normal file
View file

@ -0,0 +1,33 @@
import type { Metadata } from "next";
// Removed unused google fonts since we use local Custom Mono
import "./globals.css";
export const metadata: Metadata = {
title: "dvorakmortonxd.dev",
description: "Martin Dvořák's personal website.",
openGraph: {
title: "dvorakmortonxd.dev",
description: "Martin Dvořák's personal website.",
url: "https://dvorakmortonxd.dev",
siteName: "dvorakmortonxd.dev",
},
twitter: {
card: "summary_large_image",
title: "dvorakmortonxd.dev",
description: "Martin Dvořák's personal website.",
}
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className="antialiased">
{children}
</body>
</html>
);
}

875
src/app/page.css Normal file
View file

@ -0,0 +1,875 @@
@font-face {
font-family: 'Monocraft';
src: url('/Monocraft.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'Monocraft';
src: url('/Monocraft-Bold.ttf') format('truetype');
font-weight: bold;
font-style: normal;
}
@font-face {
font-family: 'Monocraft';
src: url('/Monocraft-Italic.ttf') format('truetype');
font-weight: normal;
font-style: italic;
}
:root {
/* Gruvbox Dark Theme */
--bg: #1d2021;
--bg-soft: #282828;
--bg-hard: #32302f;
--fg: #ebdbb2;
--gray: #a89984;
--accent: #fabd2f; /* Gruvbox Yellow */
--accent2: #fe8019; /* Gruvbox Orange */
--accent3: #d79921; /* Gruvbox Dark Yellow */
--green: #b8bb26; /* Gruvbox Green */
--aqua: #8ec07c; /* Gruvbox Aqua */
--blue: #83a598; /* Gruvbox Blue */
--purple: #d3869b; /* Gruvbox Purple */
--red: #fb4934; /* Gruvbox Red */
--darkpink: #ff0043; /* Custom Dark Pink */
--font-custom-mono: 'Monocraft', 'JetBrains Mono', 'Fira Code', 'Source Code Pro', 'Courier New', monospace;
--card-bg: rgba(40, 40, 40, 0.65);
--card-border: rgba(168, 153, 132, 0.35);
--tui-border: var(--gray);
--tui-border-highlight: var(--accent);
--radius: 0px;
}
/* RESET */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
scroll-behavior: smooth;
}
body {
min-height: 100vh;
background: var(--bg);
color: var(--fg);
font-family: var(--font-custom-mono);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow-x: hidden;
font-size: 14px;
line-height: 1.5;
}
/* Container */
.container {
width: 100%;
max-width: 1080px;
margin: 0 auto;
padding: 0 16px;
position: relative;
z-index: 1;
}
/* ============================================
HEADER - TUI top bar
============================================ */
.header {
position: sticky;
top: 0;
left: 0;
width: 100%;
z-index: 1000;
border-bottom: 1px solid var(--tui-border);
background-color: rgba(29, 32, 33, 0.4) !important;
backdrop-filter: blur(20px) saturate(150%) !important;
-webkit-backdrop-filter: blur(20px) saturate(150%) !important;
padding: 0;
}
.header-center {
max-width: 1100px;
margin: 0 auto;
display: flex;
align-items: center;
padding: 8px 16px;
gap: 10px;
}
.logo {
height: 28px;
width: 28px;
border-radius: 4px;
object-fit: cover;
border: 1px solid var(--accent);
opacity: 0.9;
transition: opacity 0.15s ease;
}
.logo:hover {
opacity: 1;
box-shadow: 0 0 8px rgba(250, 189, 47, 0.4);
}
.site-name {
font-weight: 700;
font-size: 1rem;
font-family: var(--font-custom-mono), 'JetBrains Mono', monospace;
color: var(--accent);
letter-spacing: 0.02em;
}
/* ============================================
HERO - TUI welcome panel
============================================ */
.hero {
text-align: left;
padding: 0;
position: relative;
z-index: 2;
margin-top: 16px;
}
.hero-panel {
border: 1px solid var(--accent);
background: rgba(29, 32, 33, 0.75);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
padding: 20px 24px;
position: relative;
}
.hero-panel-label {
position: absolute;
top: -1px;
left: 16px;
transform: translateY(-50%);
background: rgba(29, 32, 33, 0.95);
padding: 0 8px;
font-size: 0.8rem;
font-weight: 700;
color: var(--accent);
letter-spacing: 0.08em;
text-transform: uppercase;
}
.hero-title {
font-size: 1.6rem;
font-weight: 800;
color: var(--accent);
margin-bottom: 8px;
letter-spacing: -0.01em;
}
.hero-subtitle {
font-size: 0.9rem;
color: var(--gray);
line-height: 1.6;
max-width: none;
margin: 0;
}
.text-green {
color: var(--green);
}
.text-blue {
color: var(--blue);
}
.mail-contact {
text-align: center;
margin-bottom: 18px;
color: var(--fg);
font-size: 0.9rem;
}
.mail-contact a {
color: var(--blue);
text-decoration: none;
}
.about-section {
margin-top: 16px;
}
.about-content {
padding: 20px 24px;
color: var(--fg);
font-family: var(--font-custom-mono);
font-size: 0.9rem;
line-height: 1.6;
}
.label-blue { color: var(--blue); }
.label-yellow { color: var(--accent); }
.label-green { color: var(--green); }
.cursor-blink {
display: inline-block;
width: 8px;
height: 1.1em;
background: var(--accent);
margin-left: 4px;
vertical-align: text-bottom;
animation: cursorBlink 1s step-end infinite;
}
@keyframes cursorBlink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
/* ============================================
SOCIAL BUTTONS - TUI list style
============================================ */
.social-section {
margin-top: 16px;
}
.tui-panel {
border: 1px solid var(--tui-border);
background: rgba(29, 32, 33, 0.75);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
position: relative;
}
.tui-panel-label {
position: absolute;
top: -1px;
left: 16px;
transform: translateY(-50%);
background: rgba(29, 32, 33, 0.95);
padding: 0 8px;
font-size: 0.8rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.social-buttons {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 0;
padding: 12px 0;
}
.social-button {
display: flex;
align-items: center;
gap: 10px;
padding: 6px 20px;
background: transparent;
border: none;
color: var(--fg);
text-decoration: none;
font-weight: 500;
font-size: 0.85rem;
font-family: var(--font-custom-mono), 'JetBrains Mono', monospace;
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
position: relative;
border-bottom: 2px solid transparent;
border-radius: 0;
overflow: hidden;
z-index: 1;
}
.social-button::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(250, 189, 47, 0.2), transparent);
transition: left 0.5s ease;
z-index: -1;
}
.social-button:hover::before {
left: 100%;
}
.social-button:hover {
background: rgba(250, 189, 47, 0.08);
color: var(--accent);
}
.social-icon {
width: 16px;
height: 16px;
opacity: 0.7;
transition: opacity 0.15s ease;
}
.social-button:hover .social-icon {
opacity: 1;
filter: drop-shadow(0 0 4px rgba(250, 189, 47, 0.4));
}
.social-label {
transition: color 0.15s ease;
}
/* ============================================
SERVICES SECTION - TUI grid
============================================ */
.services-section {
margin-top: 16px;
padding-top: 0;
}
.services-title {
display: none;
}
.services-buttons {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0;
padding: 12px 0;
}
.service-button {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 20px;
background: transparent;
border: none;
border-radius: 0;
text-decoration: none;
color: var(--fg);
font-family: var(--font-custom-mono), 'JetBrains Mono', monospace;
min-width: 0;
position: relative;
overflow: hidden;
z-index: 1;
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
}
.service-button::before {
content: '';
position: absolute;
top: -100%;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(180deg, transparent, rgba(250, 189, 47, 0.15), transparent);
transition: top 0.4s ease;
z-index: -1;
}
.service-button:hover::before {
top: 100%;
}
.service-button:last-child {
border-bottom: none;
}
.service-button:hover {
background: rgba(250, 189, 47, 0.08);
transform: translateY(-3px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.service-icon {
font-size: 1.4rem;
transition: transform 0.15s ease;
width: 28px;
text-align: center;
filter: none;
}
.service-button:hover .service-icon {
transform: scale(1.1);
}
.service-button.friend .service-icon {
display: grid;
place-items: center;
width: 56px;
height: 56px;
border-radius: 50%;
overflow: hidden;
flex-shrink: 0;
}
.friend-icon {
display: block;
width: 56px;
height: 56px;
border-radius: 50%;
object-fit: cover;
}
.service-text {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 1px;
}
.service-name {
font-size: 0.9rem;
font-weight: 700;
color: var(--fg);
transition: color 0.15s ease;
}
.service-button.proxmox:hover .service-name { color: var(--blue); }
.service-button.ai:hover .service-name { color: var(--green); }
.service-button.jellyfin:hover .service-name { color: var(--purple); }
.service-button.pterodactyl:hover .service-name { color: var(--red); }
.service-button.funkwhale:hover .service-name { color: var(--darkpink); }
.service-button.mailcow:hover .service-name { color: var(--accent); }
.service-button.searxng:hover .service-name { color: var(--aqua); }
.service-button.convertx:hover .service-name { color: var(--gray); }
.service-button.gitdxd:hover .service-name { color: var(--purple); }
.service-button.proxmox { border-color: rgba(168, 153, 132, 0.1); }
.service-button.ai { border-color: rgba(168, 153, 132, 0.1); }
.service-button.jellyfin { border-color: rgba(168, 153, 132, 0.1); }
.service-button.pterodactyl { border-color: rgba(168, 153, 132, 0.1); }
.service-button.funkwhale { border-color: rgba(255, 0, 67, 0.1); }
.service-button.mailcow { border-color: rgba(168, 153, 132, 0.1); }
.service-button.searxng { border-color: rgba(168, 153, 132, 0.1); }
.service-button.convertx { border-color: rgba(168, 153, 132, 0.1); }
.service-button.gitdxd { border-color: rgba(255, 0, 67, 0.1); }
.service-desc {
font-size: 0.75rem;
color: var(--gray);
transition: color 0.15s ease;
font-family: var(--font-custom-mono), 'JetBrains Mono', monospace;
}
.service-button:hover .service-desc {
color: var(--fg);
}
/* ============================================
PROJECTS - TUI sections
============================================ */
.projects {
margin-top: 16px;
padding: 0;
text-align: left;
position: relative;
z-index: 2;
}
.section-title {
display: none;
}
.subsection-title {
display: none;
}
.subsection-wrapper {
margin-top: 16px;
}
.subsection-panel {
border: 1px solid var(--tui-border);
background: rgba(29, 32, 33, 0.75);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
position: relative;
padding: 20px 16px 16px;
}
.subsection-panel-label {
position: absolute;
top: -1px;
left: 16px;
transform: translateY(-50%);
background: rgba(29, 32, 33, 0.95);
padding: 0 8px;
font-size: 0.8rem;
font-weight: 700;
color: var(--accent2);
letter-spacing: 0.08em;
text-transform: uppercase;
}
.subsection-panel.coding-panel .subsection-panel-label {
color: var(--purple);
}
.subsection-panel.friends-panel .subsection-panel-label {
color: var(--aqua);
}
/* Project grid & cards */
.project-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(310px, 1fr));
gap: 12px;
max-width: none;
margin: 0;
padding: 0;
}
.friends-grid {
grid-template-columns: repeat(auto-fill, minmax(45%, 1fr));
}
.video-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
max-width: none;
margin: 0;
padding: 0;
}
.project-card {
background: rgba(50, 48, 47, 0.35);
border: 1px solid rgba(168, 153, 132, 0.18);
border-radius: 0;
padding: 12px;
width: 100%;
text-decoration: none;
color: inherit;
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
position: relative;
overflow: hidden;
box-shadow: none;
backdrop-filter: none;
transform: translateZ(0);
will-change: transform;
}
.project-card::after {
content: '';
position: absolute;
top: 0;
left: -150%;
width: 50%;
height: 100%;
background: linear-gradient(to right, rgba(255,255,255,0) 0%, rgba(255,255,255,0.06) 50%, rgba(255,255,255,0) 100%);
transform: skewX(-25deg);
transition: all 0.6s ease;
z-index: 2;
pointer-events: none;
}
.project-card:hover::after {
left: 167%;
}
.project-card:hover {
border-color: var(--accent);
background: rgba(50, 48, 47, 0.6);
transform: translateY(-6px) scale(1.01);
}
.project-image {
height: 160px;
width: 100%;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
border-radius: 0;
margin-bottom: 10px;
border: 1px solid rgba(168, 153, 132, 0.15);
transition: all 0.2s ease;
box-shadow: none;
}
.project-card:hover .project-image {
border-color: rgba(250, 189, 47, 0.3);
transform: none;
}
.project-title {
font-size: 0.95rem;
font-weight: 700;
color: var(--fg);
margin-bottom: 4px;
font-family: var(--font-custom-mono), 'JetBrains Mono', monospace;
transition: color 0.15s ease;
}
.project-card:hover .project-title {
color: var(--accent);
text-shadow: none;
}
.project-desc {
color: var(--gray);
font-size: 0.8rem;
line-height: 1.5;
margin-top: 4px;
font-family: var(--font-custom-mono), 'JetBrains Mono', monospace;
}
.project-card:hover .project-desc {
color: var(--fg);
}
/* Friends section styles */
.friend-card-wrapper {
display: flex;
flex-direction: column;
}
.friend-header {
display: flex;
align-items: center;
background-color: #3c3836;
padding: 4px 8px;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
}
.window-icon {
width: 16px;
height: 16px;
margin-right: 8px;
}
.friend-iframe-container {
flex-grow: 1;
overflow: hidden;
border: 1px solid var(--card-border);
border-top: none;
aspect-ratio: 2 / 1;
transform: translateZ(0);
will-change: transform;
}
.friend-iframe {
width: 100%;
height: 100%;
border: none;
}
/* ============================================
FOOTER - TUI status bar
============================================ */
.footer {
margin-top: 16px;
margin-bottom: 24px;
padding: 10px 20px;
text-align: center;
color: var(--gray);
border: 1px solid var(--tui-border);
font-size: 0.8rem;
font-family: var(--font-custom-mono), 'JetBrains Mono', monospace;
position: relative;
z-index: 2;
background: rgba(29, 32, 33, 0.75);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
box-shadow: none;
}
.footer-label {
position: absolute;
top: -1px;
right: 16px;
transform: translateY(-50%);
background: rgba(29, 32, 33, 0.95);
padding: 0 8px;
font-size: 0.7rem;
font-weight: 700;
color: var(--gray);
letter-spacing: 0.08em;
text-transform: uppercase;
}
/* ============================================
ANIMATIONS
============================================ */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.hero { animation: fadeIn 0.5s ease both; }
.social-section { animation: fadeIn 0.5s ease 0.1s both; }
.services-section { animation: fadeIn 0.5s ease 0.2s both; }
.projects { animation: fadeIn 0.5s ease 0.3s both; }
.footer { animation: fadeIn 0.5s ease 0.4s both; }
.project-card {
opacity: 0;
animation: fadeIn 0.3s ease forwards;
}
.project-grid .project-card:nth-child(1) { animation-delay: 0.05s; }
.project-grid .project-card:nth-child(2) { animation-delay: 0.1s; }
.project-grid .project-card:nth-child(3) { animation-delay: 0.15s; }
.project-grid .project-card:nth-child(4) { animation-delay: 0.2s; }
.project-grid .project-card:nth-child(5) { animation-delay: 0.25s; }
.project-grid .project-card:nth-child(6) { animation-delay: 0.3s; }
.project-grid .project-card:nth-child(7) { animation-delay: 0.35s; }
.project-grid .project-card:nth-child(8) { animation-delay: 0.4s; }
.project-grid .project-card:nth-child(9) { animation-delay: 0.45s; }
.project-grid .project-card:nth-child(10) { animation-delay: 0.5s; }
.project-grid .project-card:nth-child(11) { animation-delay: 0.55s; }
.project-grid .project-card:nth-child(12) { animation-delay: 0.6s; }
.project-grid .project-card:nth-child(13) { animation-delay: 0.65s; }
.project-grid .project-card:nth-child(14) { animation-delay: 0.7s; }
/* Custom scrollbar - thin TUI style */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg);
}
::-webkit-scrollbar-thumb {
background: var(--gray);
border: 2px solid var(--bg);
}
::-webkit-scrollbar-thumb:hover {
background: var(--accent);
}
/* Selection styling */
::selection {
background: rgba(250, 189, 47, 0.35);
color: var(--fg);
}
/* ============================================
RESPONSIVE
============================================ */
@media (max-width: 768px) {
.container {
padding: 0 8px;
}
.hero-panel {
padding: 16px 20px;
}
.hero-title {
font-size: 1.4rem;
}
.project-grid {
grid-template-columns: 1fr;
gap: 10px;
}
.social-buttons {
grid-template-columns: 1fr 1fr;
}
.services-buttons {
grid-template-columns: 1fr;
}
.friends-buttons {
grid-template-columns: 1fr;
}
.friends-grid {
grid-template-columns: 1fr;
}
.video-grid {
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.friend-iframe-container {
aspect-ratio: 16 / 9;
}
}
@media (max-width: 520px) {
.container {
padding: 0 4px;
}
.hero-title {
font-size: 1.2rem;
}
.social-buttons {
grid-template-columns: 1fr;
}
.project-grid {
grid-template-columns: 1fr;
}
.video-grid {
grid-template-columns: 1fr;
}
.project-image {
height: 130px;
}
.subsection-panel {
padding: 12px 8px;
}
}
/* Reduced motion for accessibility */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* Joker Animation */
.joker-flash-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 99999;
pointer-events: none;
display: flex;
justify-content: center;
align-items: center;
background: rgba(0, 0, 0, 0.1);
}
.joker-img {
max-width: 90vw;
max-height: 90vh;
animation: jokerFlash 1.2s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
filter: drop-shadow(0 0 50px rgba(255, 0, 67, 0.8));
}
@keyframes jokerFlash {
0% {
opacity: 0;
transform: scale(0.1);
}
15% {
opacity: 1;
transform: scale(2.5) rotate(-5deg);
}
60% {
opacity: 1;
transform: scale(1.5) rotate(5deg);
}
100% {
opacity: 0;
transform: scale(0) rotate(-20deg);
}
}

484
src/app/page.tsx Normal file
View file

@ -0,0 +1,484 @@
import Image from "next/image";
import "./page.css";
import { Metadata } from 'next';
import JokerLogo from "./components/JokerLogo";
import ServiceButton from "./components/ServiceButton";
import SlBackground from "./components/SlBackground";
export const metadata: Metadata = {
title: 'dvorakmortonxd.dev',
description:
'Welcome to my personal website, my social media links and projects lurk below... I use gentoo btw :)',
openGraph: {
title: 'dvorakmortonxd.dev',
description: 'Welcome to my personal website, my social media links and projects lurk below... I use gentoo btw :)',
url: 'https://dvorakmortonxd.dev',
siteName: 'dvorakmortonxd.dev',
},
};
// Example projects array (you can edit this)
const codingProjects = [
{
title: "Hospital Execution",
desc: "the first ever real game I made, in pygame.",
img: "/coding-1.png",
url: "https://martin-dv.itch.io/hospital-execution-demo",
},
{
title: "Underbaranoia",
desc: "basically a undertale like infinite fight simulator that gets progressively harder, made in pygame.",
img: "/coding-2.png",
url: "https://martin-dv.itch.io/underbaranoia",
},
{
title: "Offline memer",
desc: "parody of the discord bot \"Dank Memer\" made in batch. (this was technically my first ever game I published).",
img: "/coding-3.png",
url: "https://martin-dv.itch.io/offline-memer",
},
{
title: "4y",
desc: "just a renamed fork of the ly login manager (based login manager btw I love it), no extra features, just a rename, its actually outdated so dont use it.",
img: "/coding-4.png",
url: "https://git.dvorakmortonxd.dev/dvorakmortonxd/4y",
},
{
title: "4rch linux",
desc: "same as above, basically just a renamed fork of arch linux with calamares installer, also outdated and not maintained at all so no use please.",
img: "/coding-5.png",
url: "https://git.dvorakmortonxd.dev/dvorakmortonxd/4rch-linux",
},
{
title: "You_Are_An_Idiot_Virus.py",
desc: "this is just a python script that just spams blinking \"you are an idiot\" windows, made it as a prank I think, not really sure rn.",
img: "/coding-6.png",
url: "https://git.dvorakmortonxd.dev/dvorakmortonxd/uranidiot",
},
{
title: "Venturesome",
desc: "a browser I wrote in python using PyQtWebEngine with PyQt5, actually pretty cool and modular but I couldnt get past the stage of adding a tab system, basically surpassed by my other project: RustyWolf.",
img: "/coding-7.png",
url: "https://git.dvorakmortonxd.dev/dvorakmortonxd/Venturesome",
},
{
title: "Mandelbrot set in Pygame",
desc: "a python pygame script that renders the mandelbrot set, made it when I was fascinated by how it looks and ngl it still looks cool af.",
img: "/coding-8.png",
url: "https://git.dvorakmortonxd.dev/dvorakmortonxd/mandelbrot",
},
{
title: "Underpygametale",
desc: "a remake of undertale written in pygame, got it to about the flowey part (at the beginning) and didnt touch it in a long time.",
img: "/coding-9.png",
url: "https://git.dvorakmortonxd.dev/dvorakmortonxd/underpygametale",
},
{
title: "Many discord bots...",
desc: "so many of them, once I (allegedly, pls discord dont ban me) even wrote a \"self-bot\" which auto-advertised a minecraft server on minecraft discord server in lfg-hosting every two hours, around 800 people joined in total.",
img: "/coding-10.png",
url: "https://git.dvorakmortonxd.dev/dvorakmortonxd/PYGETEX",
},
{
title: "MMO minecraft plugin",
desc: "a minceraft plugin with custom bosses, items, and functions. pretty barebones and basic but I did it all manually before vibecoding was even a thing so what.",
img: "/coding-11.png",
url: "https://www.youtube.com/watch?v=cSAX5Ds61ac",
},
{
title: "Many many failed minecraft servers..",
desc: "creative games, softcraft, frostville, eusmp, eurthsmp, softsmp and many other SMPs that died later on. I cant even count how many mc servers I had in total.",
img: "/coding-12.png",
url: "https://discord.gg/KtHRm9kRmy",
},
{
title: "Swords and Sandals 2 pygame remake",
desc: "an unfinished remake of swords and sandals 2 in pygame, I got bored of it and never finished it but ill finish it one day for sure lol.",
img: "/coding-13.png",
url: "https://git.dvorakmortonxd.dev/dvorakmortonxd/sas2py",
},
{
title: "Ollama server",
desc: "my ollama vm running on my proxmox with a gpu passthroughed RTX 3070, dm me if u wanna test it lol.",
img: "/coding-14.png",
url: "https://ai.dvorakmortonxd.dev/",
},
{
title: "Batch MSDOS Simulator",
desc: "a freshman school project on making games in batch, it was like an OS simulator with a web browser, games, photos, even a video suprise (youll have to try it to figure that one out lol).",
img: "/coding-15.png",
url: "https://git.dvorakmortonxd.dev/dvorakmortonxd/DvorakMSDOS",
},
{
title: "RustyWolf",
desc: "a browser I made in rust with webkitgtk, I made it to be lightweight and to be able to be ran on macbook unibody 2010 before I realised I got scammed.. It's the successor to my previous project Venturesome :)",
img: "/coding-16.png",
url: "https://git.dvorakmortonxd.dev/dvorakmortonxd/RustyWolf",
},
{
title: "dxdwm",
desc: "a small x11 window manager written in rust.",
img: "/coding-17.png",
url: "https://git.dvorakmortonxd.dev/dvorakmortonxd/dxdwm",
},
{
title: "proxmox setup",
desc: "my proxmox setup with more than 8 vms for my services, this is running literally everything at my house.",
img: "/proxmox.png",
url: "https://proxmox.dvorakmortonxd.dev/",
},
];
export default function Home() {
const socials = [
{ name: "Main Instagram", icon: "/instagram.svg", url: "https://www.instagram.com/dvorakmortonxd/" },
{ name: "Gym Instagram", icon: "/instagram.svg", url: "https://www.instagram.com/dvorakmortonxd2/" },
{ name: "SoundCloud", icon: "/soundcloud.svg", url: "https://soundcloud.com/martin-dvorak-185636305" },
{ name: "TikTok", icon: "/tiktok.svg", url: "https://www.tiktok.com/@dvorakmortonxd" },
{ name: "Snapchat", icon: "/snapchat.svg", url: "https://www.snapchat.com/add/dvorakmortonxd" },
{ name: "GitDXD", icon: "/github.svg", url: "https://git.dvorakmortonxd.dev/dvorakmortonxd" },
{ name: "LinkedIn", icon: "/linkedin.svg", url: "https://www.linkedin.com/in/martin-dvorak-32012a394/" },
{ name: "Itch.io", icon: "/itchio.svg", url: "https://martin-dv.itch.io" },
{ name: "Discord", icon: "/discord.svg", url: "https://discord.com/users/555745543763001364" },
{ name: "Reddit", icon: "/reddit.svg", url: "https://www.reddit.com/user/M4RT1NYT/" },
{ name: "Twitter", icon: "/twitter.svg", url: "https://x.com/pepicskopcecek" },
{ name: "Quipee", icon: "/quip.svg", url: "https://quipee.me/dvorakmortonxd" },
];
const techProjects = [
{
title: "Gentoo Setup",
desc: "my main computer with gentoo. specs: Ryzen 5 3600 (6c/12t), GTX 1050 Ti (yes im aware I could use the rtx 3070 from the proxmox server but I dont need it and its much better off being in there cuz of local ai), 16gb of ram and a 240gb ssd.",
image: "/gentoo-setup.png",
},
{
title: "Proxmox Server",
desc: "my personal proxmox server (dm me on ig if u need a VPS/hosting), specs: Xeon E5 2680 v4 (14c/28t), RTX 3070 (for local ai), 68gb ddr4 rdimm ram (2*16gb, 1*32gb, 1*4gb), 256gb of sata m2 space + 1tb hdd + 750gb hdd.",
image: "/proxmox-server.png",
},
{
title: "Xbox 360 S with badupdate",
desc: "xbox 360 slim (trinity) with badupdate exploit, allowing to boot custom software. (not my photo), the specs are XCGPU (3c/6t), 0.5GB of gddr3 ram and a 500gb hdd.",
image: "/xbox360.png",
},
{
title: "Macbook Air M4 13\"",
desc: "my main device for basically everything except gaming. specs are Apple M4 (cpu:10c,gpu:8c), 16gb of unified ram (lpddr5x), 256gb Storage.",
image: "/macbook-air.png",
},
{
title: "Macbook Air 2013 11\"",
desc: "bought this for tinkering lol, specs are intel core i5 (2c/4t), 8gb of ddr3 ram and 250gb ssd, installed arch linux on it with i3-gaps (this is my laptop not josef's I gave it to him lol).",
image: "/macbook-air-11.png",
},
{
title: "iPhone 7 128gb",
desc: "somehow found a listing for 4 bucks (refurbed.cz) for it so I got it lol, it has a little dent I marked it on the photo (which isnt mine btw), except cosmetic damage its pretty much fully functional, specs are Apple A10 (4c/4t), 2gb of ram and 128gb of storage",
image: "/iphone-7.png",
},
{
title: "Macbook Unibody 2010",
desc: "found random listing on bazos for 15 bucks but after I bought a power adapter it turned out to be non functioning so I got scammed :(",
image: "/macbook2010.png",
},
{
title: "ThinkPad T400s 14\"",
desc: "got it from a random guy for 20 bucks, its an old laptop from 2009 and I disabled intel AMT and slapped void linux on it and i3wm on it, specs are intel core 2 duo (2c/2t), 8gb ddr3 ram and 800gb hdd.",
image: "/thinkpadt400s.png",
},
{
title: "Asus N56JR 15.6\"",
desc: "one of my first laptops my dad got for me when I was like 12, still works but the battery is KO, specs are intel core i7 4700HQ (4c/8t), 8gb ddr3 ram and 240gb ssd, it currently runs ubuntu linux 24.04LTS and my mom uses it for movies and shows.",
image: "/asuslaptop.png",
},
{
title: "iPhone 11 128gb",
desc: "my first iphone I got from my parents at 13, rn my dad has it because I switched over to the iphone 16 pro in 2025, specs are Apple A13 (6c/6t), 4gb of ram and 128gb of storage. (NOT MY IMAGE)",
image: "/iphone11.png",
},
{
title: "iPhone 16 Pro 128gb",
desc: "originally wanted iphone 13 mini (because of the size) but I somehow decided to get 16 pro, which is a bit smaller than my older iphone 11, specs are Apple A18 pro (6c/6t), (btw this chip is the same chip used in the macbook neo so thats cool) and 8gb of ram and 128gb of storage, my main phone.",
image: "/iphone16pro.png",
},
{
title: "Xbox one S 2TB",
desc: "my dad bought it for me also in like 2018 or 2019 and I sometimes play the console edition of minecraft because I like to play the gliding minigame on it when someone comes over.",
image: "/xboxones.png",
},
];
const friends = [
{
name: "Josef Skopec",
url: "/skopec.html",
realUrl: "https://skopec.dev/",
img: "/skopec.svg",
desc: "skopec.dev",
iframe: true
},
{
name: "Filip Bukovina",
url: "https://bukovinafilip.com/",
realUrl: "https://bukovinafilip.com/",
img: "/bukovina.svg",
desc: "bukovinafilip.com",
iframe: true
},
{
name: "Tomáš Vlk",
url: "https://vlcak.me",
realUrl: "https://vlcak.me",
img: "/vlcak.svg",
desc: "vlcak.me",
iframe: true
}
];
return <>
<SlBackground />
<div className="container">
{/* Header - TUI top bar */}
<header className="header">
<div className="header-center">
<JokerLogo />
<h1 className="site-name">dvorakmortonxd.dev</h1>
</div>
</header>
{/* Hero - TUI welcome panel */}
<main className="hero">
<div className="hero-panel">
<span className="hero-panel-label">~ README.MD</span>
<h2 className="hero-title">$ whoami</h2>
<p className="hero-subtitle">
Welcome to my website, visit the land down under to get to find out about me and my social media, services, hardware and projects. welcome to my goblin cave. {'>'}:)<span className="cursor-blink"></span>
</p>
</div>
{/* Social Links Panel */}
<div className="social-section">
<div className="tui-panel">
<span className="tui-panel-label label-blue">├─ 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>
{/* About Me Panel */}
<div className="about-section">
<div className="tui-panel">
<span className="tui-panel-label label-yellow">├─ about_me.txt</span>
<div className="about-content">
<p>
<span className="text-green">martin@dvorakmortonxd</span>:<span className="text-blue">~</span>$ cat about_me.txt
</p>
<div style={{ color: '#a3a3a3' }}>
<p>
Hey, I&#39;m Martin Dvořák — a 17-year-old IT student based in the Czech Republic, specializing in network management and administration at <span className="text-pink-700"><a href="https://sssvt.cz">sssvt.cz</a></span>. I&#39;m a freshman 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, check it.
</p>
<p>
I&#39;m a passionate homelabber and developer heavily focused on self-hosting <i>literally everything</i>, writing code, and tinkering with Linux systems and retro hardware. Whether it&#39;s configuring Proxmox, reviving an old Thinkpad with Void Linux, or compiling Gentoo on my main rig, I&#39;m always looking at bazos to get more retro hardware.
</p>
<p>
In my free time I mostly spend time with my girl {'<'}3, also constructing some of my self hosted stuff, and also when I&#39;m in my gaming mood (which is kinda rare nowadays) I play terraria (around 1300hr playtime btw) and minecraft modpacks with mekanism. I also go to the gym around 3-5 times a week, running a PPL split focused on strength/powerlifting, my SBD total is 398kg (Squat:125kg, Bench:108kg, Deadlift:165kg) with a bodyweight of 73kg.<span className="cursor-blink"></span>
</p>
</div>
</div>
</div>
</div>
{/* Services Panel */}
<div className="services-section">
<div className="tui-panel">
<span className="tui-panel-label label-green">├─ SERVICES.HTML</span>
<div className="services-buttons">
<ServiceButton
url="https://proxmox.dvorakmortonxd.dev/"
className="proxmox"
icon="🖥️"
name="Proxmox"
desc="Server Management"
/>
<ServiceButton
url="https://ai.dvorakmortonxd.dev/"
className="ai"
icon="🦾"
name="Ollama"
desc="Local AI Server"
/>
<ServiceButton
url="https://jelly.dvorakmortonxd.dev/"
className="jellyfin"
icon="🎬"
name="Jellyfin"
desc="Local Media Server"
/>
<ServiceButton
url="https://fake-pterodactyl.dvorakmortonxd.dev/"
className="pterodactyl"
icon="📦"
name="MCSManager"
desc="Game Server Panel"
/>
<ServiceButton
url="https://navidrome.dvorakmortonxd.dev/"
className="funkwhale"
icon="🎸"
name="Navidrome"
desc="Music Streaming"
/>
<ServiceButton
url="https://mailcow.dvorakmortonxd.dev/"
className="mailcow"
icon="💌"
name="Mailcow"
desc="Email Server"
/>
<ServiceButton
url="https://search.dvorakmortonxd.dev/"
className="searxng"
icon="🔦"
name="SearxNG"
desc="Private Search"
/>
<ServiceButton
url="https://convert.dvorakmortonxd.dev/"
className="convertx"
icon="📽️"
name="ConvertX"
desc="File Converter"
/>
<ServiceButton
url="https://git.dvorakmortonxd.dev"
className="gitdxd"
icon="🐙"
name="GitDXD"
desc="Better Github"
/>
</div>
</div>
</div>
{/* Softcraft Panel */}
<div className="services-section" style={{ marginTop: '20px' }}>
<div className="tui-panel">
<span className="tui-panel-label" style={{ color: '#d946ef' }}>├─ SOFTCRAFT.JAR</span>
<div className="services-buttons" style={{ gap: '1rem' }}>
<ServiceButton
url="https://dvorakmortonxd.dev/softcraft"
checkUrl="minecraft://46.30.237.43:6767"
className="softcraft"
icon="⛏️"
name="Softcraft"
desc="Play on my Minecraft Server!"
/>
<ServiceButton
url="https://bluemap.dvorakmortonxd.dev"
className="bluemap"
icon="🗺️"
name="BlueMap"
desc="Live SoftCraft Map"
/>
<ServiceButton
url="https://discord-sc.dvorakmortonxd.dev"
className="discord-sc"
icon={<Image src="/discord.svg" alt="Discord" width={28} height={28} />}
name="Discord"
desc="Join our community"
/>
</div>
</div>
</div>
</main>
{/* Projects */}
<section className="projects">
{/* Tech Section */}
<div className="subsection-wrapper">
<div className="subsection-panel">
<span className="subsection-panel-label">├─ TECH_LIST.JSON</span>
<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>
{/* Coding Section */}
<div className="subsection-wrapper">
<div className="subsection-panel coding-panel">
<span className="subsection-panel-label">├─ PROJECTS.DOCX</span>
<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>
{/* Friends Section */}
<div className="subsection-wrapper">
<div className="subsection-panel friends-panel">
<span className="subsection-panel-label">└─ FRIENDS.TSX</span>
<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>
</div>
</section>
<footer className="footer">
<span className="footer-label">v1.6.7</span>
© 2025-2026 Martin Dvořák. All rights reserved (not really I dont care)
</footer>
</div>
</>
}

227
src/app/softcraft/page.tsx Normal file
View file

@ -0,0 +1,227 @@
import Image from "next/image";
import "../page.css";
import { Metadata } from 'next';
import JokerLogo from "../components/JokerLogo";
import ServiceButton from "../components/ServiceButton";
export const metadata: Metadata = {
title: 'dvorakmortonxd.dev/softcraft',
description: 'Welcome to the Softcraft Minecraft Server info page!',
openGraph: {
title: 'dvorakmortonxd.dev/softcraft',
description: 'Welcome to the Softcraft Minecraft Server info page!',
url: 'https://dvorakmortonxd.dev/softcraft',
siteName: 'dvorakmortonxd.dev',
},
};
export default function Softcraft() {
return <>
<style>{`
@keyframes mc-panorama-pan {
0% { background-position: 0% center; }
100% { background-position: 100% center; }
}
.header, .hero-panel, .tui-panel, .footer, .service-button, .social-button, .video-container, .hero-panel-label, .tui-panel-label, .footer-label {
background-image: linear-gradient(rgba(10, 10, 10, 0.85), rgba(10, 10, 10, 0.85)), url(/dirt.png) !important;
background-repeat: repeat !important;
background-size: 64px !important;
image-rendering: pixelated !important;
background-color: transparent !important;
}
.terminal-box {
background-image: linear-gradient(rgba(0, 0, 0, 0.95), rgba(0, 0, 0, 0.95)), url(/dirt.png) !important;
background-repeat: repeat !important;
background-size: 64px !important;
image-rendering: pixelated !important;
}
`}</style>
<div style={{
position: 'fixed',
top: 0,
left: 0,
width: '100vw',
height: '100vh',
zIndex: -1,
backgroundImage: 'url(/minecrafttest2.png)',
backgroundSize: 'cover',
animation: 'mc-panorama-pan 120s linear infinite alternate',
filter: 'brightness(0.6)'
}} />
<div className="container">
{/* Header - TUI top bar */}
<header className="header">
<div className="header-center">
<JokerLogo />
<h1 className="site-name">dvorakmortonxd.dev/softcraft</h1>
</div>
</header>
{/* Hero - TUI welcome panel */}
<main className="hero">
<div className="hero-panel">
<span className="hero-panel-label">~ ABOUT_SOFTCRAFT.MD</span>
<h2 className="hero-title">$ ./start_softcraft.sh</h2>
<p className="hero-subtitle">
Welcome to the Softcraft info page! {'>'}:)<span className="cursor-blink"></span>
</p>
</div>
{/* Server Info Panel */}
<div className="about-section">
<div className="tui-panel">
<span className="tui-panel-label label-yellow">├─ neofetch</span>
<div className="about-content">
<p>
<span className="text-green">martin@dvorakmortonxd</span>:<span className="text-blue">~/softcraft</span>$ neofetch
</p>
<div style={{ display: 'flex', gap: '2rem', marginTop: '1.5rem', alignItems: 'center', flexWrap: 'wrap', color: '#a3a3a3' }}>
<div style={{ flexShrink: 0 }}>
<Image
src="/softcraft.png"
alt="Softcraft Logo"
width={128}
height={128}
style={{ imageRendering: 'pixelated', borderRadius: '8px' }}
/>
</div>
<div style={{ flex: 1, minWidth: '250px', lineHeight: '1.6' }}>
<div className="terminal-box" style={{ padding: '15px', borderRadius: '8px', border: '1px dashed #444', marginTop: '-10px' }}>
<p style={{ margin: '0 0 5px 0', color: '#22c55e', fontWeight: 'bold' }}>martin<span style={{ color: '#fff' }}>@</span>softcraft</p>
<p style={{ margin: '0 0 10px 0' }}>----------------</p>
<p style={{ margin: '0' }}><strong>OS</strong>: Purpur 1.21.11 or newer on Debian 13 Trixie</p>
<p style={{ margin: '0' }}><strong>Host/IP Adress</strong>: <span style={{ color: '#fff' }}>mc.dvorakmortonxd.dev</span></p>
<p style={{ margin: '0' }}><strong>Uptime</strong>: 24/7</p>
<p style={{ margin: '0' }}><strong>Rules</strong>: Little to no chat control, just enjoy the game bro</p>
</div>
<div style={{ display: 'flex', gap: '0.5rem', marginTop: '15px' }}>
<span style={{ width: '16px', height: '16px', backgroundColor: '#000000', display: 'inline-block' }}></span>
<span style={{ width: '16px', height: '16px', backgroundColor: '#ef4444', display: 'inline-block' }}></span>
<span style={{ width: '16px', height: '16px', backgroundColor: '#22c55e', display: 'inline-block' }}></span>
<span style={{ width: '16px', height: '16px', backgroundColor: '#eab308', display: 'inline-block' }}></span>
<span style={{ width: '16px', height: '16px', backgroundColor: '#3b82f6', display: 'inline-block' }}></span>
<span style={{ width: '16px', height: '16px', backgroundColor: '#d946ef', display: 'inline-block' }}></span>
<span style={{ width: '16px', height: '16px', backgroundColor: '#06b6d4', display: 'inline-block' }}></span>
<span style={{ width: '16px', height: '16px', backgroundColor: '#ffffff', display: 'inline-block' }}></span>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Links Panel */}
<div className="services-section" style={{ marginTop: '20px' }}>
<div className="tui-panel">
<span className="tui-panel-label label-blue">├─ LINKS.SH</span>
<div className="services-buttons" style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '1rem' }}>
<ServiceButton
url="https://bluemap.dvorakmortonxd.dev"
className="bluemap"
icon="🗺️"
name="Live Server Map"
desc="View the world in 3D"
/>
<ServiceButton
url="https://discord-sc.dvorakmortonxd.dev"
className="discord-sc"
icon={<Image src="/discord.svg" alt="Discord" width={28} height={28} />}
name="Community Discord"
desc="Chat with other players"
/>
<ServiceButton
url="https://dvorakmortonxd.dev"
className="back-home"
icon="🏠"
name="Main Site"
desc="cd .."
/>
</div>
</div>
</div>
{/* Vote Panel */}
<div className="services-section" style={{ marginTop: '20px' }}>
<div className="tui-panel">
<span className="tui-panel-label label-yellow">├─ VOTE_HERE.JS</span>
<div className="services-buttons" style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '1rem' }}>
<ServiceButton
url="https://czech-craft.eu/server/softcraft-refreshed/"
className="vote"
icon="⭐"
name="Czech-Craft"
desc="Vote for us!"
/>
<ServiceButton
url="https://www.minecraft-list.cz/server/softcraft-refreshed"
className="vote"
icon="⭐"
name="Minecraft-List.cz"
desc="Vote for us!"
/>
<ServiceButton
url="https://minecraftservery.eu/server/softcraft-refreshed"
className="vote"
icon="⭐"
name="MinecraftServery.eu"
desc="Vote for us!"
/>
<ServiceButton
url="https://craftlist.org/softcraft"
className="vote"
icon="⭐"
name="CraftList.org"
desc="Vote for us!"
/>
</div>
</div>
</div>
{/* Video Panel */}
<div className="video-section" style={{ marginTop: '20px' }}>
<div className="tui-panel">
<span className="tui-panel-label" style={{ color: '#ec4899' }}>└─ BASH &#39;LS ./VIDEOS/&#39;</span>
<div className="video-grid" style={{ marginTop: '10px' }}>
{[
"ifBnNS8X3O8",
"fQ3T374cpZk",
"yYrzQkhp8CQ",
"gIl01RaQjcI",
"e3gjtyPNZjg",
"P97CC3BWvg4",
"uPNN6LppOsg",
"NCy-h29lfzM"
].map(id => (
<div key={id} className="video-container" style={{ width: '100%', aspectRatio: '16/9', borderRadius: '8px', overflow: 'hidden', transform: 'translateZ(0)' }}>
<iframe
width="100%"
height="100%"
src={`https://www.youtube.com/embed/${id}`}
title="YouTube video player"
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowFullScreen
loading="lazy"
style={{ border: 'none' }}
></iframe>
</div>
))}
</div>
</div>
</div>
</main>
<footer className="footer">
<span className="footer-label">v1.6.7</span>
© 2025-2026 Martin Dvořák. All rights reserved
</footer>
</div>
</>
}

BIN
src/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

27
tsconfig.json Normal file
View file

@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}